Skip to content

Commit f31efc5

Browse files
committed
internal/lsp: add an orphaned file diagnostic for nested modules
This change adds a diagnostic and error message that appears when a user edits a nested module in legacy mode. At first, I thought a diagnostic would be enough, but it's actually quite difficult to spot it when you have a bunch of "undeclared name" diagnostics caused by the nested module, so I figured a progress bar error message would also be useful. This error message just indicates to the user that they should open the nested module as its own workspace folder. Also, while debugging this, I noticed that command-line-arguments packages can have test variants, which we were never handling. So I addressed that in this change. Fixes golang/go#42109 Change-Id: Ifa6f6af401a3725835c09b76e35f889ec5cb8901 Reviewed-on: https://go-review.googlesource.com/c/tools/+/275554 Trust: Rebecca Stambler <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent b71f123 commit f31efc5

File tree

6 files changed

+168
-44
lines changed

6 files changed

+168
-44
lines changed

gopls/internal/regtest/diagnostics_test.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1664,7 +1664,7 @@ package b
16641664
env.Await(
16651665
env.DiagnosticAtRegexp("a/a.go", "package a"),
16661666
env.DiagnosticAtRegexp("b/go.mod", "module b.com"),
1667-
OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires one module per workspace folder."),
1667+
OutstandingWork(lsp.WorkspaceLoadFailure, "gopls requires a module at the root of your workspace."),
16681668
)
16691669
})
16701670
})
@@ -1693,6 +1693,74 @@ package b
16931693
})
16941694
}
16951695

1696+
func TestNestedModules(t *testing.T) {
1697+
const proxy = `
1698+
-- [email protected]/go.mod --
1699+
module nested.com
1700+
1701+
go 1.12
1702+
-- [email protected]/hello/hello.go --
1703+
package hello
1704+
1705+
func Hello() {}
1706+
`
1707+
1708+
const nested = `
1709+
-- go.mod --
1710+
module mod.com
1711+
1712+
go 1.12
1713+
1714+
require nested.com v1.0.0
1715+
-- go.sum --
1716+
nested.com v1.0.0 h1:I6spLE4CgFqMdBPc+wTV2asDO2QJ3tU0YAT+jkLeN1I=
1717+
nested.com v1.0.0/go.mod h1:ly53UzXQgVjSlV7wicdBB4p8BxfytuGT1Xcyv0ReJfI=
1718+
-- main.go --
1719+
package main
1720+
1721+
import "nested.com/hello"
1722+
1723+
func main() {
1724+
hello.Hello()
1725+
}
1726+
-- nested/go.mod --
1727+
module nested.com
1728+
1729+
-- nested/hello/hello.go --
1730+
package hello
1731+
1732+
func Hello() {
1733+
helloHelper()
1734+
}
1735+
-- nested/hello/hello_helper.go --
1736+
package hello
1737+
1738+
func helloHelper() {}
1739+
`
1740+
withOptions(
1741+
WithProxyFiles(proxy),
1742+
WithModes(Singleton),
1743+
).run(t, nested, func(t *testing.T, env *Env) {
1744+
// Expect a diagnostic in a nested module.
1745+
env.OpenFile("nested/hello/hello.go")
1746+
didOpen := CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), 1)
1747+
env.Await(
1748+
OnceMet(
1749+
didOpen,
1750+
env.DiagnosticAtRegexp("nested/hello/hello.go", "helloHelper"),
1751+
),
1752+
OnceMet(
1753+
didOpen,
1754+
env.DiagnosticAtRegexpWithMessage("nested/hello/hello.go", "package hello", "nested module"),
1755+
),
1756+
OnceMet(
1757+
didOpen,
1758+
OutstandingWork(lsp.WorkspaceLoadFailure, "nested module"),
1759+
),
1760+
)
1761+
})
1762+
}
1763+
16961764
func TestAdHocPackagesReloading(t *testing.T) {
16971765
const nomod = `
16981766
-- main.go --

internal/lsp/cache/load.go

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
5454
for _, scope := range scopes {
5555
switch scope := scope.(type) {
5656
case packagePath:
57-
if scope == "command-line-arguments" {
57+
if isCommandLineArguments(string(scope)) {
5858
panic("attempted to load command-line-arguments")
5959
}
6060
// The only time we pass package paths is when we're doing a
@@ -204,12 +204,6 @@ func (s *snapshot) parseLoadError(ctx context.Context, loadErr error) *source.Cr
204204
// workspaceLayoutErrors returns a diagnostic for every open file, as well as
205205
// an error message if there are no open files.
206206
func (s *snapshot) WorkspaceLayoutError(ctx context.Context) *source.CriticalError {
207-
// Assume the workspace is misconfigured only if we've detected an invalid
208-
// build configuration. Currently, a valid build configuration is either a
209-
// module at the root of the view or a GOPATH workspace.
210-
if s.ValidBuildConfiguration() {
211-
return nil
212-
}
213207
if len(s.workspace.getKnownModFiles()) == 0 {
214208
return nil
215209
}
@@ -219,33 +213,70 @@ func (s *snapshot) WorkspaceLayoutError(ctx context.Context) *source.CriticalErr
219213
if s.workspace.moduleSource != legacyWorkspace {
220214
return nil
221215
}
222-
// The user's workspace contains go.mod files and they have
223-
// GO111MODULE=on, so we should guide them to create a
224-
// workspace folder for each module.
225-
226-
// Add a diagnostic to every open file, or return a general error if
227-
// there aren't any.
228-
var open []source.VersionedFileHandle
229-
s.mu.Lock()
230-
for _, fh := range s.files {
231-
if s.isOpenLocked(fh.URI()) {
232-
open = append(open, fh)
233-
}
216+
// If the user has one module per view, there is nothing to warn about.
217+
if s.ValidBuildConfiguration() && len(s.workspace.getKnownModFiles()) == 1 {
218+
return nil
234219
}
235-
s.mu.Unlock()
236220

237-
msg := `gopls requires one module per workspace folder.
221+
// Apply diagnostics about the workspace configuration to relevant open
222+
// files.
223+
openFiles := s.openFiles()
224+
225+
// If the snapshot does not have a valid build configuration, it may be
226+
// that the user has opened a directory that contains multiple modules.
227+
// Check for that an warn about it.
228+
if !s.ValidBuildConfiguration() {
229+
msg := `gopls requires a module at the root of your workspace.
238230
You can work with multiple modules by opening each one as a workspace folder.
239231
Improvements to this workflow will be coming soon (https://github.com/golang/go/issues/32394),
240232
and you can learn more here: https://github.com/golang/go/issues/36899.`
241-
242-
criticalError := &source.CriticalError{
243-
MainError: errors.New(msg),
233+
return &source.CriticalError{
234+
MainError: errors.Errorf(msg),
235+
ErrorList: s.applyCriticalErrorToFiles(ctx, msg, openFiles),
236+
}
244237
}
245-
if len(open) == 0 {
246-
return criticalError
238+
239+
// If the user has one active go.mod file, they may still be editing files
240+
// in nested modules. Check the module of each open file and add warnings
241+
// that the nested module must be opened as a workspace folder.
242+
if len(s.workspace.getActiveModFiles()) == 1 {
243+
// Get the active root go.mod file to compare against.
244+
var rootModURI span.URI
245+
for uri := range s.workspace.getActiveModFiles() {
246+
rootModURI = uri
247+
}
248+
nestedModules := map[span.URI][]source.VersionedFileHandle{}
249+
for _, fh := range openFiles {
250+
modURI := moduleForURI(s.workspace.knownModFiles, fh.URI())
251+
if modURI != rootModURI {
252+
nestedModules[modURI] = append(nestedModules[modURI], fh)
253+
}
254+
}
255+
// Add a diagnostic to each file in a nested module to mark it as
256+
// "orphaned". Don't show a general diagnostic in the progress bar,
257+
// because the user may still want to edit a file in a nested module.
258+
var srcErrs []*source.Error
259+
for modURI, uris := range nestedModules {
260+
msg := fmt.Sprintf(`This file is in %s, which is a nested module in the %s module.
261+
gopls currently requires one module per workspace folder.
262+
Please open %s as a separate workspace folder.
263+
You can learn more here: https://github.com/golang/go/issues/36899.
264+
`, modURI.Filename(), rootModURI.Filename(), modURI.Filename())
265+
srcErrs = append(srcErrs, s.applyCriticalErrorToFiles(ctx, msg, uris)...)
266+
}
267+
if len(srcErrs) != 0 {
268+
return &source.CriticalError{
269+
MainError: errors.Errorf(`You are working in a nested module. Please open it as a separate workspace folder.`),
270+
ErrorList: srcErrs,
271+
}
272+
}
247273
}
248-
for _, fh := range open {
274+
return nil
275+
}
276+
277+
func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []source.VersionedFileHandle) []*source.Error {
278+
var srcErrs []*source.Error
279+
for _, fh := range files {
249280
// Place the diagnostics on the package or module declarations.
250281
var rng protocol.Range
251282
switch fh.Kind() {
@@ -263,14 +294,14 @@ and you can learn more here: https://github.com/golang/go/issues/36899.`
263294
}
264295
}
265296
}
266-
criticalError.ErrorList = append(criticalError.ErrorList, &source.Error{
297+
srcErrs = append(srcErrs, &source.Error{
267298
URI: fh.URI(),
268299
Range: rng,
269300
Kind: source.ListError,
270301
Message: msg,
271302
})
272303
}
273-
return criticalError
304+
return srcErrs
274305
}
275306

276307
type workspaceDirKey string

internal/lsp/cache/snapshot.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.Invocat
294294
modURI = span.URIFromPath(filepath.Join(tmpDir.Filename(), "go.mod"))
295295
}
296296
} else {
297-
modURI = s.GoModForFile(ctx, span.URIFromPath(inv.WorkingDir))
297+
modURI = s.GoModForFile(span.URIFromPath(inv.WorkingDir))
298298
}
299299

300300
var modContent []byte
@@ -795,9 +795,13 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac
795795
return results, nil
796796
}
797797

798-
func (s *snapshot) GoModForFile(ctx context.Context, uri span.URI) span.URI {
798+
func (s *snapshot) GoModForFile(uri span.URI) span.URI {
799+
return moduleForURI(s.workspace.activeModFiles, uri)
800+
}
801+
802+
func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI {
799803
var match span.URI
800-
for modURI := range s.workspace.getActiveModFiles() {
804+
for modURI := range modFiles {
801805
if !source.InDir(dirURI(modURI).Filename(), uri.Filename()) {
802806
continue
803807
}
@@ -890,7 +894,7 @@ func (s *snapshot) addID(uri span.URI, id packageID) {
890894
}
891895
// If we are setting a real ID, when the package had only previously
892896
// had a command-line-arguments ID, we should just replace it.
893-
if existingID == "command-line-arguments" {
897+
if isCommandLineArguments(string(existingID)) {
894898
s.ids[uri][i] = id
895899
// Delete command-line-arguments if it was a workspace package.
896900
delete(s.workspacePackages, existingID)
@@ -900,6 +904,14 @@ func (s *snapshot) addID(uri span.URI, id packageID) {
900904
s.ids[uri] = append(s.ids[uri], id)
901905
}
902906

907+
// isCommandLineArguments reports whether a given value denotes
908+
// "command-line-arguments" package, which is a package with an unknown ID
909+
// created by the go command. It can have a test variant, which is why callers
910+
// should not check that a value equals "command-line-arguments" directly.
911+
func isCommandLineArguments(s string) bool {
912+
return strings.Contains(s, "command-line-arguments")
913+
}
914+
903915
func (s *snapshot) isWorkspacePackage(id packageID) (packagePath, bool) {
904916
s.mu.Lock()
905917
defer s.mu.Unlock()
@@ -962,6 +974,19 @@ func (s *snapshot) IsOpen(uri span.URI) bool {
962974

963975
}
964976

977+
func (s *snapshot) openFiles() []source.VersionedFileHandle {
978+
s.mu.Lock()
979+
defer s.mu.Unlock()
980+
981+
var open []source.VersionedFileHandle
982+
for _, fh := range s.files {
983+
if s.isOpenLocked(fh.URI()) {
984+
open = append(open, fh)
985+
}
986+
}
987+
return open
988+
}
989+
965990
func (s *snapshot) isOpenLocked(uri span.URI) bool {
966991
_, open := s.files[uri].(*overlay)
967992
return open
@@ -1012,7 +1037,7 @@ func (s *snapshot) reloadWorkspace(ctx context.Context) error {
10121037
missingMetadata = true
10131038

10141039
// Don't try to reload "command-line-arguments" directly.
1015-
if pkgPath == "command-line-arguments" {
1040+
if isCommandLineArguments(string(pkgPath)) {
10161041
continue
10171042
}
10181043
pkgPathSet[pkgPath] = struct{}{}
@@ -1361,7 +1386,7 @@ copyIDs:
13611386
// go command when the user is outside of GOPATH and outside of a
13621387
// module. Do not cache them as workspace packages for longer than
13631388
// necessary.
1364-
if id == "command-line-arguments" {
1389+
if isCommandLineArguments(string(id)) {
13651390
if invalidateMetadata, ok := transitiveIDs[id]; invalidateMetadata && ok {
13661391
continue
13671392
}

internal/lsp/code_action.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ func diagnosticToAnalyzer(snapshot source.Snapshot, src, msg string) (analyzer *
363363
var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
364364

365365
func goGetFixes(ctx context.Context, snapshot source.Snapshot, uri span.URI, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
366-
if snapshot.GoModForFile(ctx, uri) == "" {
366+
if snapshot.GoModForFile(uri) == "" {
367367
// Go get only supports module mode for now.
368368
return nil, nil
369369
}
@@ -510,7 +510,7 @@ func moduleQuickFixes(ctx context.Context, snapshot source.Snapshot, fh source.V
510510
case source.Mod:
511511
modFH = fh
512512
case source.Go:
513-
modURI := snapshot.GoModForFile(ctx, fh.URI())
513+
modURI := snapshot.GoModForFile(fh.URI())
514514
if modURI == "" {
515515
return nil, nil
516516
}

internal/lsp/diagnostics.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAn
186186
// with the user's workspace layout. Workspace packages that only have the
187187
// ID "command-line-arguments" are usually a symptom of a bad workspace
188188
// configuration.
189-
if onlyCommandLineArguments(wsPkgs) {
189+
if containsCommandLineArguments(wsPkgs) {
190190
if criticalErr := snapshot.WorkspaceLayoutError(ctx); criticalErr != nil {
191191
err = criticalErr
192192
}
@@ -563,11 +563,11 @@ func (s *Server) shouldIgnoreError(ctx context.Context, snapshot source.Snapshot
563563
return !hasGo
564564
}
565565

566-
func onlyCommandLineArguments(pkgs []source.Package) bool {
566+
func containsCommandLineArguments(pkgs []source.Package) bool {
567567
for _, pkg := range pkgs {
568-
if pkg.ID() != "command-line-arguments" {
569-
return false
568+
if strings.Contains(pkg.ID(), "command-line-arguments") {
569+
return true
570570
}
571571
}
572-
return true
572+
return false
573573
}

internal/lsp/source/view.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ type Snapshot interface {
118118
ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error)
119119

120120
// GoModForFile returns the URI of the go.mod file for the given URI.
121-
GoModForFile(ctx context.Context, uri span.URI) span.URI
121+
GoModForFile(uri span.URI) span.URI
122122

123123
// BuiltinPackage returns information about the special builtin package.
124124
BuiltinPackage(ctx context.Context) (*BuiltinPackage, error)

0 commit comments

Comments
 (0)