1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package regtest
6
7import (
8	"path/filepath"
9	"strings"
10	"testing"
11
12	"golang.org/x/tools/internal/lsp"
13	"golang.org/x/tools/internal/lsp/protocol"
14	"golang.org/x/tools/internal/lsp/tests"
15	"golang.org/x/tools/internal/testenv"
16)
17
18const proxy = `
19-- example.com@v1.2.3/go.mod --
20module example.com
21
22go 1.12
23-- example.com@v1.2.3/blah/blah.go --
24package blah
25
26const Name = "Blah"
27-- random.org@v1.2.3/go.mod --
28module random.org
29
30go 1.12
31-- random.org@v1.2.3/blah/blah.go --
32package hello
33
34const Name = "Hello"
35`
36
37func TestModFileModification(t *testing.T) {
38	testenv.NeedsGo1Point(t, 14)
39
40	const untidyModule = `
41-- a/go.mod --
42module mod.com
43
44-- a/main.go --
45package main
46
47import "example.com/blah"
48
49func main() {
50	println(blah.Name)
51}
52`
53
54	runner := runMultiple{
55		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
56		{"nested", withOptions(ProxyFiles(proxy))},
57	}
58
59	t.Run("basic", func(t *testing.T) {
60		runner.run(t, untidyModule, func(t *testing.T, env *Env) {
61			// Open the file and make sure that the initial workspace load does not
62			// modify the go.mod file.
63			goModContent := env.ReadWorkspaceFile("a/go.mod")
64			env.OpenFile("a/main.go")
65			env.Await(
66				env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""),
67			)
68			if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent {
69				t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got))
70			}
71			// Save the buffer, which will format and organize imports.
72			// Confirm that the go.mod file still does not change.
73			env.SaveBuffer("a/main.go")
74			env.Await(
75				env.DiagnosticAtRegexp("a/main.go", "\"example.com/blah\""),
76			)
77			if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent {
78				t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got))
79			}
80		})
81	})
82
83	// Reproduce golang/go#40269 by deleting and recreating main.go.
84	t.Run("delete main.go", func(t *testing.T) {
85		t.Skip("This test will be flaky until golang/go#40269 is resolved.")
86
87		runner.run(t, untidyModule, func(t *testing.T, env *Env) {
88			goModContent := env.ReadWorkspaceFile("a/go.mod")
89			mainContent := env.ReadWorkspaceFile("a/main.go")
90			env.OpenFile("a/main.go")
91			env.SaveBuffer("a/main.go")
92
93			env.RemoveWorkspaceFile("a/main.go")
94			env.Await(
95				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1),
96				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), 1),
97				CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), 2),
98			)
99
100			env.WriteWorkspaceFile("main.go", mainContent)
101			env.Await(
102				env.DiagnosticAtRegexp("main.go", "\"example.com/blah\""),
103			)
104			if got := env.ReadWorkspaceFile("go.mod"); got != goModContent {
105				t.Fatalf("go.mod changed on disk:\n%s", tests.Diff(t, goModContent, got))
106			}
107		})
108	})
109}
110
111func TestGoGetFix(t *testing.T) {
112	testenv.NeedsGo1Point(t, 14)
113	const mod = `
114-- a/go.mod --
115module mod.com
116
117go 1.12
118
119-- a/main.go --
120package main
121
122import "example.com/blah"
123
124var _ = blah.Name
125`
126
127	const want = `module mod.com
128
129go 1.12
130
131require example.com v1.2.3
132`
133
134	runMultiple{
135		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
136		{"nested", withOptions(ProxyFiles(proxy))},
137	}.run(t, mod, func(t *testing.T, env *Env) {
138		if strings.Contains(t.Name(), "workspace_module") {
139			t.Skip("workspace module mode doesn't set -mod=readonly")
140		}
141		env.OpenFile("a/main.go")
142		var d protocol.PublishDiagnosticsParams
143		env.Await(
144			OnceMet(
145				env.DiagnosticAtRegexp("a/main.go", `"example.com/blah"`),
146				ReadDiagnostics("a/main.go", &d),
147			),
148		)
149		var goGetDiag protocol.Diagnostic
150		for _, diag := range d.Diagnostics {
151			if strings.Contains(diag.Message, "could not import") {
152				goGetDiag = diag
153			}
154		}
155		env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{goGetDiag})
156		if got := env.ReadWorkspaceFile("a/go.mod"); got != want {
157			t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
158		}
159	})
160}
161
162// Tests that multiple missing dependencies gives good single fixes.
163func TestMissingDependencyFixes(t *testing.T) {
164	testenv.NeedsGo1Point(t, 14)
165	const mod = `
166-- a/go.mod --
167module mod.com
168
169go 1.12
170
171-- a/main.go --
172package main
173
174import "example.com/blah"
175import "random.org/blah"
176
177var _, _ = blah.Name, hello.Name
178`
179
180	const want = `module mod.com
181
182go 1.12
183
184require random.org v1.2.3
185`
186
187	runMultiple{
188		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
189		{"nested", withOptions(ProxyFiles(proxy))},
190	}.run(t, mod, func(t *testing.T, env *Env) {
191		env.OpenFile("a/main.go")
192		var d protocol.PublishDiagnosticsParams
193		env.Await(
194			OnceMet(
195				env.DiagnosticAtRegexp("a/main.go", `"random.org/blah"`),
196				ReadDiagnostics("a/main.go", &d),
197			),
198		)
199		var randomDiag protocol.Diagnostic
200		for _, diag := range d.Diagnostics {
201			if strings.Contains(diag.Message, "random.org") {
202				randomDiag = diag
203			}
204		}
205		env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag})
206		if got := env.ReadWorkspaceFile("a/go.mod"); got != want {
207			t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
208		}
209	})
210}
211
212func TestIndirectDependencyFix(t *testing.T) {
213	testenv.NeedsGo1Point(t, 14)
214
215	const mod = `
216-- a/go.mod --
217module mod.com
218
219go 1.12
220
221require example.com v1.2.3 // indirect
222-- a/go.sum --
223example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
224example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
225-- a/main.go --
226package main
227
228import "example.com/blah"
229
230func main() {
231	fmt.Println(blah.Name)
232`
233	const want = `module mod.com
234
235go 1.12
236
237require example.com v1.2.3
238`
239
240	runMultiple{
241		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
242		{"nested", withOptions(ProxyFiles(proxy))},
243	}.run(t, mod, func(t *testing.T, env *Env) {
244		env.OpenFile("a/go.mod")
245		var d protocol.PublishDiagnosticsParams
246		env.Await(
247			OnceMet(
248				env.DiagnosticAtRegexp("a/go.mod", "// indirect"),
249				ReadDiagnostics("a/go.mod", &d),
250			),
251		)
252		env.ApplyQuickFixes("a/go.mod", d.Diagnostics)
253		if got := env.Editor.BufferText("a/go.mod"); got != want {
254			t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
255		}
256	})
257}
258
259func TestUnusedDiag(t *testing.T) {
260	testenv.NeedsGo1Point(t, 14)
261
262	const proxy = `
263-- example.com@v1.0.0/x.go --
264package pkg
265const X = 1
266`
267	const files = `
268-- a/go.mod --
269module mod.com
270go 1.14
271require example.com v1.0.0
272-- a/go.sum --
273example.com v1.0.0 h1:38O7j5rEBajXk+Q5wzLbRN7KqMkSgEiN9NqcM1O2bBM=
274example.com v1.0.0/go.mod h1:vUsPMGpx9ZXXzECCOsOmYCW7npJTwuA16yl89n3Mgls=
275-- a/main.go --
276package main
277func main() {}
278`
279
280	const want = `module mod.com
281
282go 1.14
283`
284
285	runMultiple{
286		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
287		{"nested", withOptions(ProxyFiles(proxy))},
288	}.run(t, files, func(t *testing.T, env *Env) {
289		env.OpenFile("a/go.mod")
290		var d protocol.PublishDiagnosticsParams
291		env.Await(
292			OnceMet(
293				env.DiagnosticAtRegexp("a/go.mod", `require example.com`),
294				ReadDiagnostics("a/go.mod", &d),
295			),
296		)
297		env.ApplyQuickFixes("a/go.mod", d.Diagnostics)
298		if got := env.ReadWorkspaceFile("a/go.mod"); got != want {
299			t.Fatalf("unexpected go.mod content:\n%s", tests.Diff(t, want, got))
300		}
301	})
302}
303
304// Test to reproduce golang/go#39041. It adds a new require to a go.mod file
305// that already has an unused require.
306func TestNewDepWithUnusedDep(t *testing.T) {
307	testenv.NeedsGo1Point(t, 14)
308
309	const proxy = `
310-- github.com/esimov/caire@v1.2.5/go.mod --
311module github.com/esimov/caire
312
313go 1.12
314-- github.com/esimov/caire@v1.2.5/caire.go --
315package caire
316
317func RemoveTempImage() {}
318-- google.golang.org/protobuf@v1.20.0/go.mod --
319module google.golang.org/protobuf
320
321go 1.12
322-- google.golang.org/protobuf@v1.20.0/hello/hello.go --
323package hello
324`
325	const repro = `
326-- a/go.mod --
327module mod.com
328
329go 1.14
330
331require google.golang.org/protobuf v1.20.0
332-- a/go.sum --
333github.com/esimov/caire v1.2.5 h1:OcqDII/BYxcBYj3DuwDKjd+ANhRxRqLa2n69EGje7qw=
334github.com/esimov/caire v1.2.5/go.mod h1:mXnjRjg3+WUtuhfSC1rKRmdZU9vJZyS1ZWU0qSvJhK8=
335google.golang.org/protobuf v1.20.0 h1:y9T1vAtFKQg0faFNMOxJU7WuEqPWolVkjIkU6aI8qCY=
336google.golang.org/protobuf v1.20.0/go.mod h1:FcqsytGClbtLv1ot8NvsJHjBi0h22StKVP+K/j2liKA=
337-- a/main.go --
338package main
339
340import (
341    "github.com/esimov/caire"
342)
343
344func _() {
345    caire.RemoveTempImage()
346}`
347
348	runMultiple{
349		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
350		{"nested", withOptions(ProxyFiles(proxy))},
351	}.run(t, repro, func(t *testing.T, env *Env) {
352		env.OpenFile("a/main.go")
353		var d protocol.PublishDiagnosticsParams
354		env.Await(
355			OnceMet(
356				env.DiagnosticAtRegexp("a/main.go", `"github.com/esimov/caire"`),
357				ReadDiagnostics("a/main.go", &d),
358			),
359		)
360		env.ApplyQuickFixes("a/main.go", d.Diagnostics)
361		want := `module mod.com
362
363go 1.14
364
365require (
366	github.com/esimov/caire v1.2.5
367	google.golang.org/protobuf v1.20.0
368)
369`
370		if got := env.ReadWorkspaceFile("a/go.mod"); got != want {
371			t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", tests.Diff(t, want, got))
372		}
373	})
374}
375
376// TODO: For this test to be effective, the sandbox's file watcher must respect
377// the file watching GlobPattern in the capability registration. See
378// golang/go#39384.
379func TestModuleChangesOnDisk(t *testing.T) {
380	testenv.NeedsGo1Point(t, 14)
381
382	const mod = `
383-- a/go.mod --
384module mod.com
385
386go 1.12
387
388require example.com v1.2.3
389-- a/go.sum --
390example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
391example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
392-- a/main.go --
393package main
394
395func main() {
396	fmt.Println(blah.Name)
397`
398	runMultiple{
399		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
400		{"nested", withOptions(ProxyFiles(proxy))},
401	}.run(t, mod, func(t *testing.T, env *Env) {
402		env.Await(env.DiagnosticAtRegexp("a/go.mod", "require"))
403		env.RunGoCommandInDir("a", "mod", "tidy")
404		env.Await(
405			EmptyDiagnostics("a/go.mod"),
406		)
407	})
408}
409
410// Tests golang/go#39784: a missing indirect dependency, necessary
411// due to blah@v2.0.0's incomplete go.mod file.
412func TestBadlyVersionedModule(t *testing.T) {
413	testenv.NeedsGo1Point(t, 14)
414
415	const proxy = `
416-- example.com/blah/@v/v1.0.0.mod --
417module example.com
418
419go 1.12
420-- example.com/blah@v1.0.0/blah.go --
421package blah
422
423const Name = "Blah"
424-- example.com/blah/v2/@v/v2.0.0.mod --
425module example.com
426
427go 1.12
428-- example.com/blah/v2@v2.0.0/blah.go --
429package blah
430
431import "example.com/blah"
432
433var _ = blah.Name
434const Name = "Blah"
435`
436	const files = `
437-- a/go.mod --
438module mod.com
439
440go 1.12
441
442require example.com/blah/v2 v2.0.0
443-- a/go.sum --
444example.com/blah v1.0.0 h1:kGPlWJbMsn1P31H9xp/q2mYI32cxLnCvauHN0AVaHnc=
445example.com/blah v1.0.0/go.mod h1:PZUQaGFeVjyDmAE8ywmLbmDn3fj4Ws8epg4oLuDzW3M=
446example.com/blah/v2 v2.0.0 h1:w5baE9JuuU11s3de3yWx2sU05AhNkgLYdZ4qukv+V0k=
447example.com/blah/v2 v2.0.0/go.mod h1:UZiKbTwobERo/hrqFLvIQlJwQZQGxWMVY4xere8mj7w=
448-- a/main.go --
449package main
450
451import "example.com/blah/v2"
452
453var _ = blah.Name
454`
455	runMultiple{
456		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
457		{"nested", withOptions(ProxyFiles(proxy))},
458	}.run(t, files, func(t *testing.T, env *Env) {
459		env.OpenFile("a/main.go")
460		env.OpenFile("a/go.mod")
461		var d protocol.PublishDiagnosticsParams
462		env.Await(
463			OnceMet(
464				DiagnosticAt("a/go.mod", 0, 0),
465				ReadDiagnostics("a/go.mod", &d),
466			),
467		)
468		env.ApplyQuickFixes("a/main.go", d.Diagnostics)
469		const want = `module mod.com
470
471go 1.12
472
473require (
474	example.com/blah v1.0.0 // indirect
475	example.com/blah/v2 v2.0.0
476)
477`
478		env.Await(EmptyDiagnostics("a/go.mod"))
479		if got := env.Editor.BufferText("a/go.mod"); got != want {
480			t.Fatalf("suggested fixes failed:\n%s", tests.Diff(t, want, got))
481		}
482	})
483}
484
485// Reproduces golang/go#38232.
486func TestUnknownRevision(t *testing.T) {
487	testenv.NeedsGo1Point(t, 14)
488
489	const unknown = `
490-- a/go.mod --
491module mod.com
492
493require (
494	example.com v1.2.2
495)
496-- a/main.go --
497package main
498
499import "example.com/blah"
500
501func main() {
502	var x = blah.Name
503}
504`
505
506	runner := runMultiple{
507		{"default", withOptions(ProxyFiles(proxy), WorkspaceFolders("a"))},
508		{"nested", withOptions(ProxyFiles(proxy))},
509	}
510	// Start from a bad state/bad IWL, and confirm that we recover.
511	t.Run("bad", func(t *testing.T) {
512		runner.run(t, unknown, func(t *testing.T, env *Env) {
513			env.OpenFile("a/go.mod")
514			env.Await(
515				env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
516			)
517			env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3")
518			env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
519
520			d := protocol.PublishDiagnosticsParams{}
521			env.Await(
522				OnceMet(
523					env.DiagnosticAtRegexpWithMessage("a/go.mod", "example.com v1.2.3", "example.com@v1.2.3"),
524					ReadDiagnostics("a/go.mod", &d),
525				),
526			)
527			env.ApplyQuickFixes("a/go.mod", d.Diagnostics)
528
529			env.Await(
530				EmptyDiagnostics("a/go.mod"),
531				env.DiagnosticAtRegexp("a/main.go", "x = "),
532			)
533		})
534	})
535
536	const known = `
537-- a/go.mod --
538module mod.com
539
540require (
541	example.com v1.2.3
542)
543-- a/go.sum --
544example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
545example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
546-- a/main.go --
547package main
548
549import "example.com/blah"
550
551func main() {
552	var x = blah.Name
553}
554`
555	// Start from a good state, transform to a bad state, and confirm that we
556	// still recover.
557	t.Run("good", func(t *testing.T) {
558		runner.run(t, known, func(t *testing.T, env *Env) {
559			env.OpenFile("a/go.mod")
560			env.Await(
561				env.DiagnosticAtRegexp("a/main.go", "x = "),
562			)
563			env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2")
564			env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
565			env.Await(
566				env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.2"),
567			)
568			env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3")
569			env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk
570			env.Await(
571				env.DiagnosticAtRegexp("a/main.go", "x = "),
572			)
573		})
574	})
575}
576
577// Confirm that an error in an indirect dependency of a requirement is surfaced
578// as a diagnostic in the go.mod file.
579func TestErrorInIndirectDependency(t *testing.T) {
580	testenv.NeedsGo1Point(t, 14)
581
582	const badProxy = `
583-- example.com@v1.2.3/go.mod --
584module example.com
585
586go 1.12
587
588require random.org v1.2.3 // indirect
589-- example.com@v1.2.3/blah/blah.go --
590package blah
591
592const Name = "Blah"
593-- random.org@v1.2.3/go.mod --
594module bob.org
595
596go 1.12
597-- random.org@v1.2.3/blah/blah.go --
598package hello
599
600const Name = "Hello"
601`
602	const module = `
603-- a/go.mod --
604module mod.com
605
606go 1.14
607
608require example.com v1.2.3
609-- a/main.go --
610package main
611
612import "example.com/blah"
613
614func main() {
615	println(blah.Name)
616}
617`
618	runMultiple{
619		{"default", withOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))},
620		{"nested", withOptions(ProxyFiles(badProxy))},
621	}.run(t, module, func(t *testing.T, env *Env) {
622		env.OpenFile("a/go.mod")
623		env.Await(
624			env.DiagnosticAtRegexp("a/go.mod", "require example.com v1.2.3"),
625		)
626	})
627}
628
629// A copy of govim's config_set_env_goflags_mod_readonly test.
630func TestGovimModReadonly(t *testing.T) {
631	const mod = `
632-- go.mod --
633module mod.com
634
635go 1.13
636-- main.go --
637package main
638
639import "example.com/blah"
640
641func main() {
642	println(blah.Name)
643}
644`
645	withOptions(
646		EditorConfig{
647			Env: map[string]string{
648				"GOFLAGS": "-mod=readonly",
649			},
650		},
651		ProxyFiles(proxy),
652		Modes(Singleton),
653	).run(t, mod, func(t *testing.T, env *Env) {
654		env.OpenFile("main.go")
655		original := env.ReadWorkspaceFile("go.mod")
656		env.Await(
657			env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
658		)
659		got := env.ReadWorkspaceFile("go.mod")
660		if got != original {
661			t.Fatalf("go.mod file modified:\n%s", tests.Diff(t, original, got))
662		}
663		env.RunGoCommand("get", "example.com/blah@v1.2.3")
664		env.RunGoCommand("mod", "tidy")
665		env.Await(
666			EmptyDiagnostics("main.go"),
667		)
668	})
669}
670
671func TestMultiModuleModDiagnostics(t *testing.T) {
672	testenv.NeedsGo1Point(t, 14)
673
674	const mod = `
675-- a/go.mod --
676module mod.com
677
678go 1.14
679
680require (
681	example.com v1.2.3
682)
683-- a/main.go --
684package main
685
686func main() {}
687-- b/go.mod --
688module mod.com
689
690go 1.14
691-- b/main.go --
692package main
693
694import "example.com/blah"
695
696func main() {
697	blah.SaySomething()
698}
699`
700	withOptions(
701		ProxyFiles(workspaceProxy),
702		Modes(Experimental),
703	).run(t, mod, func(t *testing.T, env *Env) {
704		env.Await(
705			env.DiagnosticAtRegexp("a/go.mod", "example.com v1.2.3"),
706			env.DiagnosticAtRegexp("b/go.mod", "module mod.com"),
707		)
708	})
709}
710
711func TestModTidyWithBuildTags(t *testing.T) {
712	testenv.NeedsGo1Point(t, 14)
713
714	const mod = `
715-- go.mod --
716module mod.com
717
718go 1.14
719-- main.go --
720// +build bob
721
722package main
723
724import "example.com/blah"
725
726func main() {
727	blah.SaySomething()
728}
729`
730	withOptions(
731		ProxyFiles(workspaceProxy),
732		EditorConfig{
733			BuildFlags: []string{"-tags", "bob"},
734		},
735	).run(t, mod, func(t *testing.T, env *Env) {
736		env.Await(
737			env.DiagnosticAtRegexp("main.go", `"example.com/blah"`),
738		)
739	})
740}
741
742func TestModTypoDiagnostic(t *testing.T) {
743	const mod = `
744-- go.mod --
745module mod.com
746
747go 1.12
748-- main.go --
749package main
750
751func main() {}
752`
753	run(t, mod, func(t *testing.T, env *Env) {
754		env.OpenFile("go.mod")
755		env.RegexpReplace("go.mod", "module", "modul")
756		env.Await(
757			env.DiagnosticAtRegexp("go.mod", "modul"),
758		)
759	})
760}
761
762func TestSumUpdateFixesDiagnostics(t *testing.T) {
763	testenv.NeedsGo1Point(t, 14)
764
765	const mod = `
766-- go.mod --
767module mod.com
768
769go 1.12
770
771require (
772	example.com v1.2.3
773)
774-- go.sum --
775-- main.go --
776package main
777
778import (
779	"example.com/blah"
780)
781
782func main() {
783	println(blah.Name)
784}
785`
786	withOptions(
787		Modes(Singleton), // workspace modules don't use -mod=readonly (golang/go#43346)
788		ProxyFiles(workspaceProxy),
789	).run(t, mod, func(t *testing.T, env *Env) {
790		d := &protocol.PublishDiagnosticsParams{}
791		env.OpenFile("go.mod")
792		env.Await(
793			OnceMet(
794				DiagnosticAt("go.mod", 0, 0),
795				ReadDiagnostics("go.mod", d),
796			),
797		)
798		env.ApplyQuickFixes("go.mod", d.Diagnostics)
799		env.Await(
800			EmptyDiagnostics("go.mod"),
801		)
802	})
803}
804
805// This test confirms that editing a go.mod file only causes metadata
806// to be invalidated when it's saved.
807func TestGoModInvalidatesOnSave(t *testing.T) {
808	const mod = `
809-- go.mod --
810module mod.com
811
812go 1.12
813-- main.go --
814package main
815
816func main() {
817	hello()
818}
819-- hello.go --
820package main
821
822func hello() {}
823`
824	withOptions(
825		// TODO(rFindley) this doesn't work in multi-module workspace mode, because
826		// it keeps around the last parsing modfile. Update this test to also
827		// exercise the workspace module.
828		Modes(Singleton),
829	).run(t, mod, func(t *testing.T, env *Env) {
830		env.OpenFile("go.mod")
831		env.Await(CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1))
832		env.RegexpReplace("go.mod", "module", "modul")
833		// Confirm that we still have metadata with only on-disk edits.
834		env.OpenFile("main.go")
835		file, _ := env.GoToDefinition("main.go", env.RegexpSearch("main.go", "hello"))
836		if filepath.Base(file) != "hello.go" {
837			t.Fatalf("expected definition in hello.go, got %s", file)
838		}
839		// Confirm that we no longer have metadata when the file is saved.
840		env.SaveBufferWithoutActions("go.mod")
841		_, _, err := env.Editor.GoToDefinition(env.Ctx, "main.go", env.RegexpSearch("main.go", "hello"))
842		if err == nil {
843			t.Fatalf("expected error, got none")
844		}
845	})
846}
847
848func TestRemoveUnusedDependency(t *testing.T) {
849	testenv.NeedsGo1Point(t, 14)
850
851	const proxy = `
852-- hasdep.com@v1.2.3/go.mod --
853module hasdep.com
854
855go 1.12
856
857require example.com v1.2.3
858-- hasdep.com@v1.2.3/a/a.go --
859package a
860-- example.com@v1.2.3/go.mod --
861module example.com
862
863go 1.12
864-- example.com@v1.2.3/blah/blah.go --
865package blah
866
867const Name = "Blah"
868-- random.com@v1.2.3/go.mod --
869module random.com
870
871go 1.12
872-- random.com@v1.2.3/blah/blah.go --
873package blah
874
875const Name = "Blah"
876`
877	t.Run("almost tidied", func(t *testing.T) {
878		const mod = `
879-- go.mod --
880module mod.com
881
882go 1.12
883
884require hasdep.com v1.2.3
885-- go.sum --
886example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
887example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
888hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI=
889hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes=
890-- main.go --
891package main
892
893func main() {}
894`
895		withOptions(
896			ProxyFiles(proxy),
897		).run(t, mod, func(t *testing.T, env *Env) {
898			d := &protocol.PublishDiagnosticsParams{}
899			env.Await(
900				OnceMet(
901					env.DiagnosticAtRegexp("go.mod", "require hasdep.com v1.2.3"),
902					ReadDiagnostics("go.mod", d),
903				),
904			)
905			const want = `module mod.com
906
907go 1.12
908`
909			env.ApplyQuickFixes("go.mod", d.Diagnostics)
910			if got := env.ReadWorkspaceFile("go.mod"); got != want {
911				t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got))
912			}
913		})
914	})
915
916	t.Run("not tidied", func(t *testing.T) {
917		const mod = `
918-- go.mod --
919module mod.com
920
921go 1.12
922
923require hasdep.com v1.2.3
924require random.com v1.2.3
925-- go.sum --
926example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY=
927example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
928hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI=
929hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes=
930random.com v1.2.3 h1:PzYTykzqqH6+qU0dIgh9iPFbfb4Mm8zNBjWWreRKtx0=
931random.com v1.2.3/go.mod h1:8EGj+8a4Hw1clAp8vbaeHAsKE4sbm536FP7nKyXO+qQ=
932-- main.go --
933package main
934
935func main() {}
936`
937		withOptions(
938			ProxyFiles(proxy),
939		).run(t, mod, func(t *testing.T, env *Env) {
940			d := &protocol.PublishDiagnosticsParams{}
941			env.OpenFile("go.mod")
942			pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3")
943			env.Await(
944				OnceMet(
945					DiagnosticAt("go.mod", pos.Line, pos.Column),
946					ReadDiagnostics("go.mod", d),
947				),
948			)
949			const want = `module mod.com
950
951go 1.12
952
953require random.com v1.2.3
954`
955			var diagnostics []protocol.Diagnostic
956			for _, d := range d.Diagnostics {
957				if d.Range.Start.Line != float64(pos.Line) {
958					continue
959				}
960				diagnostics = append(diagnostics, d)
961			}
962			env.ApplyQuickFixes("go.mod", diagnostics)
963			if got := env.Editor.BufferText("go.mod"); got != want {
964				t.Fatalf("unexpected content in go.mod:\n%s", tests.Diff(t, want, got))
965			}
966		})
967	})
968}
969
970func TestSumUpdateQuickFix(t *testing.T) {
971	// Error messages changed in 1.16 that changed the diagnostic positions.
972	testenv.NeedsGo1Point(t, 16)
973
974	const mod = `
975-- go.mod --
976module mod.com
977
978go 1.12
979
980require (
981	example.com v1.2.3
982)
983-- go.sum --
984-- main.go --
985package main
986
987import (
988	"example.com/blah"
989)
990
991func main() {
992	blah.Hello()
993}
994`
995	withOptions(
996		ProxyFiles(workspaceProxy),
997		Modes(Singleton),
998	).run(t, mod, func(t *testing.T, env *Env) {
999		env.OpenFile("go.mod")
1000		pos := env.RegexpSearch("go.mod", "example.com")
1001		params := &protocol.PublishDiagnosticsParams{}
1002		env.Await(
1003			OnceMet(
1004				env.DiagnosticAtRegexp("go.mod", "example.com"),
1005				ReadDiagnostics("go.mod", params),
1006			),
1007		)
1008		var diagnostic protocol.Diagnostic
1009		for _, d := range params.Diagnostics {
1010			if d.Range.Start.Line == float64(pos.Line) {
1011				diagnostic = d
1012				break
1013			}
1014		}
1015		env.ApplyQuickFixes("go.mod", []protocol.Diagnostic{diagnostic})
1016		const want = `example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c=
1017example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo=
1018`
1019		if got := env.ReadWorkspaceFile("go.sum"); got != want {
1020			t.Fatalf("unexpected go.sum contents:\n%s", tests.Diff(t, want, got))
1021		}
1022	})
1023}
1024