Skip to content

Commit 57089f8

Browse files
committed
internal/lsp: remove dependencies using text edits when necessary
golang/go#43335 explains the issues with using `go get module@none`, which will only be resolved in Go 1.17. In the meantime, we use the go command whenever possible, but if the module is not tidied, we have to use textual edits instead. This means the go.sum file will not be accurately updated to remove the dependency, but unfortunately, I don't believe there is anything that we can do in that case. Fixes golang/go#43335 Change-Id: I771f68f34a6136e73e9dd82b692ed4c235c3b293 Reviewed-on: https://go-review.googlesource.com/c/tools/+/279716 Trust: Rebecca Stambler <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 834755c commit 57089f8

File tree

3 files changed

+211
-10
lines changed

3 files changed

+211
-10
lines changed

gopls/internal/regtest/modfile_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,3 +816,126 @@ func hello() {}
816816
}
817817
})
818818
}
819+
820+
func TestRemoveUnusedDependency(t *testing.T) {
821+
testenv.NeedsGo1Point(t, 14)
822+
823+
const proxy = `
824+
-- [email protected]/go.mod --
825+
module hasdep.com
826+
827+
go 1.12
828+
829+
require example.com v1.2.3
830+
-- [email protected]/a/a.go --
831+
package a
832+
-- [email protected]/go.mod --
833+
module example.com
834+
835+
go 1.12
836+
-- [email protected]/blah/blah.go --
837+
package blah
838+
839+
const Name = "Blah"
840+
-- [email protected]/go.mod --
841+
module random.com
842+
843+
go 1.12
844+
-- [email protected]/blah/blah.go --
845+
package blah
846+
847+
const Name = "Blah"
848+
`
849+
t.Run("almost tidied", func(t *testing.T) {
850+
const mod = `
851+
-- go.mod --
852+
module mod.com
853+
854+
go 1.12
855+
856+
require hasdep.com v1.2.3
857+
-- go.sum --
858+
example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
859+
example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
860+
hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI=
861+
hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes=
862+
-- main.go --
863+
package main
864+
865+
func main() {}
866+
`
867+
withOptions(
868+
ProxyFiles(proxy),
869+
).run(t, mod, func(t *testing.T, env *Env) {
870+
d := &protocol.PublishDiagnosticsParams{}
871+
env.Await(
872+
OnceMet(
873+
env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"),
874+
ReadDiagnostics("go.mod", d),
875+
),
876+
)
877+
const want = `module mod.com
878+
879+
go 1.12
880+
`
881+
env.ApplyQuickFixes("go.mod", d.Diagnostics)
882+
if got := env.ReadWorkspaceFile("go.mod"); got != want {
883+
t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got))
884+
}
885+
})
886+
})
887+
888+
t.Run("not tidied", func(t *testing.T) {
889+
const mod = `
890+
-- go.mod --
891+
module mod.com
892+
893+
go 1.12
894+
895+
require hasdep.com v1.2.3
896+
require random.com v1.2.3
897+
-- go.sum --
898+
example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
899+
example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
900+
hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI=
901+
hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes=
902+
random.com v1.2.3 h1:PzYTykzqqH6+qU0dIgh9iPFbfb4Mm8zNBjWWreRKtx0=
903+
random.com v1.2.3/go.mod h1:8EGj+8a4Hw1clAp8vbaeHAsKE4sbm536FP7nKyXO+qQ=
904+
-- main.go --
905+
package main
906+
907+
func main() {}
908+
`
909+
withOptions(
910+
ProxyFiles(proxy),
911+
).run(t, mod, func(t *testing.T, env *Env) {
912+
d := &protocol.PublishDiagnosticsParams{}
913+
env.OpenFile("go.mod")
914+
pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3")
915+
env.Await(
916+
OnceMet(
917+
DiagnosticAt("go.mod", pos.Line, pos.Column),
918+
ReadDiagnostics("go.mod", d),
919+
),
920+
)
921+
const want = `module mod.com
922+
923+
go 1.12
924+
925+
require random.com v1.2.3
926+
`
927+
var diagnostics []protocol.Diagnostic
928+
for _, d := range d.Diagnostics {
929+
if d.Range.Start.Line != float64(pos.Line) {
930+
continue
931+
}
932+
diagnostics = append(diagnostics, d)
933+
}
934+
env.ApplyQuickFixes("go.mod", diagnostics)
935+
if got := env.Editor.BufferText("go.mod"); got != want {
936+
t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got))
937+
}
938+
})
939+
})
940+
941+
}

internal/lsp/cache/mod_tidy.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,6 @@ func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.Par
274274
}
275275
delete(unused, req.Mod.Path)
276276
}
277-
for _, req := range unused {
278-
srcErr, err := unusedError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
279-
if err != nil {
280-
return nil, err
281-
}
282-
errors = append(errors, srcErr)
283-
}
284277
for _, req := range wrongDirectness {
285278
// Handle dependencies that are incorrectly labeled indirect and
286279
// vice versa.
@@ -380,16 +373,25 @@ func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.Par
380373
}
381374
}
382375
}
376+
// Finally, add errors for any unused dependencies.
377+
onlyError := len(errors) == 0 && len(unused) == 1
378+
for _, req := range unused {
379+
srcErr, err := unusedError(pm.Mapper, req, onlyError, snapshot.View().Options().ComputeEdits)
380+
if err != nil {
381+
return nil, err
382+
}
383+
errors = append(errors, srcErr)
384+
}
383385
return errors, nil
384386
}
385387

386388
// unusedError returns a source.Error for an unused require.
387-
func unusedError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) {
389+
func unusedError(m *protocol.ColumnMapper, req *modfile.Require, onlyError bool, computeEdits diff.ComputeEdits) (*source.Error, error) {
388390
rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
389391
if err != nil {
390392
return nil, err
391393
}
392-
args, err := source.MarshalArgs(m.URI, false, []string{req.Mod.Path + "@none"})
394+
args, err := source.MarshalArgs(m.URI, onlyError, req.Mod.Path)
393395
if err != nil {
394396
return nil, err
395397
}

internal/lsp/command.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"path/filepath"
1515
"strings"
1616

17+
"golang.org/x/mod/modfile"
1718
"golang.org/x/tools/internal/event"
1819
"golang.org/x/tools/internal/gocommand"
1920
"golang.org/x/tools/internal/lsp/cache"
@@ -216,7 +217,7 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source
216217
return err
217218
}
218219
return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
219-
case source.CommandAddDependency, source.CommandUpgradeDependency, source.CommandRemoveDependency:
220+
case source.CommandAddDependency, source.CommandUpgradeDependency:
220221
var uri protocol.DocumentURI
221222
var goCmdArgs []string
222223
var addRequire bool
@@ -229,6 +230,56 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source
229230
return err
230231
}
231232
return s.runGoGetModule(ctx, snapshot, uri.SpanURI(), addRequire, goCmdArgs)
233+
case source.CommandRemoveDependency:
234+
var uri protocol.DocumentURI
235+
var modulePath string
236+
var onlyError bool
237+
if err := source.UnmarshalArgs(args, &uri, &onlyError, &modulePath); err != nil {
238+
return err
239+
}
240+
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
241+
defer release()
242+
if !ok {
243+
return err
244+
}
245+
// If the module is tidied apart from the one unused diagnostic, we can
246+
// run `go get module@none`, and then run `go mod tidy`. Otherwise, we
247+
// must make textual edits.
248+
// TODO(rstambler): In Go 1.17+, we will be able to use the go command
249+
// without checking if the module is tidy.
250+
if onlyError {
251+
if err := s.runGoGetModule(ctx, snapshot, uri.SpanURI(), false, []string{modulePath + "@none"}); err != nil {
252+
return err
253+
}
254+
return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
255+
}
256+
pm, err := snapshot.ParseMod(ctx, fh)
257+
if err != nil {
258+
return err
259+
}
260+
edits, err := dropDependency(snapshot, pm, modulePath)
261+
if err != nil {
262+
return err
263+
}
264+
response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
265+
Edit: protocol.WorkspaceEdit{
266+
DocumentChanges: []protocol.TextDocumentEdit{{
267+
TextDocument: protocol.VersionedTextDocumentIdentifier{
268+
Version: fh.Version(),
269+
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
270+
URI: protocol.URIFromSpanURI(fh.URI()),
271+
},
272+
},
273+
Edits: edits,
274+
}},
275+
},
276+
})
277+
if err != nil {
278+
return err
279+
}
280+
if !response.Applied {
281+
return fmt.Errorf("edits not applied because of %s", response.FailureReason)
282+
}
232283
case source.CommandGoGetPackage:
233284
var uri protocol.DocumentURI
234285
var pkg string
@@ -303,6 +354,31 @@ func (s *Server) runCommand(ctx context.Context, work *workDone, command *source
303354
return nil
304355
}
305356

357+
// dropDependency returns the edits to remove the given require from the go.mod
358+
// file.
359+
func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
360+
// We need a private copy of the parsed go.mod file, since we're going to
361+
// modify it.
362+
copied, err := modfile.Parse("", pm.Mapper.Content, nil)
363+
if err != nil {
364+
return nil, err
365+
}
366+
if err := copied.DropRequire(modulePath); err != nil {
367+
return nil, err
368+
}
369+
copied.Cleanup()
370+
newContent, err := copied.Format()
371+
if err != nil {
372+
return nil, err
373+
}
374+
// Calculate the edits to be made due to the change.
375+
diff, err := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent))
376+
if err != nil {
377+
return nil, err
378+
}
379+
return source.ToProtocolEdits(pm.Mapper, diff)
380+
}
381+
306382
func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI, work *workDone, tests, benchmarks []string) error {
307383
pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
308384
if err != nil {

0 commit comments

Comments
 (0)