internal/lsp: report diagnostics on go.work files

Report diagnostics on use lines where the directory doesn't have a
go.mod file, and on syntax errors in go.work.

For golang/go#50930

Change-Id: Idab36b43d86c4842f8eecd5c071ce0587e6f27b3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/389317
Trust: Michael Matloob <matloob@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 82bfd12..4ff6f72 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -796,6 +796,39 @@
 	})
 }
 
+func TestUseGoWorkDiagnosticMissingModule(t *testing.T) {
+	const files = `
+-- go.work --
+go 1.18
+
+use ./foo
+`
+	Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("go.work")
+		env.Await(
+			env.DiagnosticAtRegexpWithMessage("go.work", "use", "directory ./foo does not contain a module"),
+		)
+		t.Log("bar")
+	})
+}
+
+func TestUseGoWorkDiagnosticSyntaxError(t *testing.T) {
+	const files = `
+-- go.work --
+go 1.18
+
+usa ./foo
+replace
+`
+	Run(t, files, func(t *testing.T, env *Env) {
+		env.OpenFile("go.work")
+		env.Await(
+			env.DiagnosticAtRegexpWithMessage("go.work", "usa", "unknown directive: usa"),
+			env.DiagnosticAtRegexpWithMessage("go.work", "replace", "usage: replace"),
+		)
+	})
+}
+
 func TestNonWorkspaceFileCreation(t *testing.T) {
 	testenv.NeedsGo1Point(t, 13)
 
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index b555b9a..8a2d42a 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -73,13 +73,13 @@
 				if err != nil {
 					return &parseModData{err: err}
 				}
-				parseErrors = []*source.Diagnostic{{
+				parseErrors = append(parseErrors, &source.Diagnostic{
 					URI:      modFH.URI(),
 					Range:    rng,
 					Severity: protocol.SeverityError,
 					Source:   source.ParseError,
 					Message:  mfErr.Err.Error(),
-				}}
+				})
 			}
 		}
 		return &parseModData{
@@ -131,7 +131,7 @@
 
 		contents, err := modFH.Read()
 		if err != nil {
-			return &parseModData{err: err}
+			return &parseWorkData{err: err}
 		}
 		m := &protocol.ColumnMapper{
 			URI:       modFH.URI(),
@@ -144,20 +144,20 @@
 		if parseErr != nil {
 			mfErrList, ok := parseErr.(modfile.ErrorList)
 			if !ok {
-				return &parseModData{err: fmt.Errorf("unexpected parse error type %v", parseErr)}
+				return &parseWorkData{err: fmt.Errorf("unexpected parse error type %v", parseErr)}
 			}
 			for _, mfErr := range mfErrList {
 				rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
 				if err != nil {
-					return &parseModData{err: err}
+					return &parseWorkData{err: err}
 				}
-				parseErrors = []*source.Diagnostic{{
+				parseErrors = append(parseErrors, &source.Diagnostic{
 					URI:      modFH.URI(),
 					Range:    rng,
 					Severity: protocol.SeverityError,
 					Source:   source.ParseError,
 					Message:  mfErr.Err.Error(),
-				}}
+				})
 			}
 		}
 		return &parseWorkData{
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index e786c02..1554fbe 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -162,6 +162,10 @@
 	return uris
 }
 
+func (s *snapshot) WorkFile() span.URI {
+	return s.workspace.workFile
+}
+
 func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle {
 	s.mu.Lock()
 	defer s.mu.Unlock()
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index e352e4b..3bf8122 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -21,6 +21,7 @@
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/template"
+	"golang.org/x/tools/internal/lsp/work"
 	"golang.org/x/tools/internal/span"
 	"golang.org/x/tools/internal/xcontext"
 	errors "golang.org/x/xerrors"
@@ -35,6 +36,7 @@
 	analysisSource
 	typeCheckSource
 	orphanedSource
+	workSource
 )
 
 // A diagnosticReport holds results for a single diagnostic source.
@@ -210,6 +212,23 @@
 		s.storeDiagnostics(snapshot, id.URI, modSource, diags)
 	}
 
+	// Diagnose the go.work file, if it exists.
+	workReports, workErr := work.Diagnostics(ctx, snapshot)
+	if ctx.Err() != nil {
+		log.Trace.Log(ctx, "diagnose cancelled")
+		return
+	}
+	if workErr != nil {
+		event.Error(ctx, "warning: diagnose go.work", workErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID()))
+	}
+	for id, diags := range workReports {
+		if id.URI == "" {
+			event.Error(ctx, "missing URI for work file diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
+			continue
+		}
+		s.storeDiagnostics(snapshot, id.URI, workSource, diags)
+	}
+
 	// Diagnose all of the packages in the workspace.
 	wsPkgs, err := snapshot.ActivePackages(ctx)
 	if s.shouldIgnoreError(ctx, snapshot, err) {
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index f18aaf7..b26bae7 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -14,7 +14,6 @@
 	"golang.org/x/tools/internal/lsp/command"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
-	"golang.org/x/tools/internal/span"
 )
 
 // LensFuncs returns the supported lensFuncs for go.mod files.
@@ -129,7 +128,7 @@
 		return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI())
 	}
 	syntax := pm.File.Module.Syntax
-	return lineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End)
+	return source.LineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End)
 }
 
 // firstRequireRange returns the range for the first "require" in the given
@@ -150,19 +149,5 @@
 	if start.Byte == 0 || firstRequire.Start.Byte < start.Byte {
 		start, end = firstRequire.Start, firstRequire.End
 	}
-	return lineToRange(pm.Mapper, fh.URI(), start, end)
-}
-
-func lineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) {
-	line, col, err := m.Converter.ToPosition(start.Byte)
-	if err != nil {
-		return protocol.Range{}, err
-	}
-	s := span.NewPoint(line, col, start.Byte)
-	line, col, err = m.Converter.ToPosition(end.Byte)
-	if err != nil {
-		return protocol.Range{}, err
-	}
-	e := span.NewPoint(line, col, end.Byte)
-	return m.Range(span.New(uri, s, e))
+	return source.LineToRange(pm.Mapper, fh.URI(), start, end)
 }
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 4b4d0cb..9c49d8b 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -61,7 +61,7 @@
 		if !ok || req.Mod.Version == ver {
 			continue
 		}
-		rng, err := lineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
+		rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 962419b..e41c1d4 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -17,6 +17,7 @@
 	"strconv"
 	"strings"
 
+	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/span"
 	errors "golang.org/x/xerrors"
@@ -563,3 +564,18 @@
 	size := tok.Pos(tok.Size())
 	return int(pos) >= tok.Base() && pos <= size
 }
+
+// LineToRange creates a Range spanning start and end.
+func LineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) {
+	line, col, err := m.Converter.ToPosition(start.Byte)
+	if err != nil {
+		return protocol.Range{}, err
+	}
+	s := span.NewPoint(line, col, start.Byte)
+	line, col, err = m.Converter.ToPosition(end.Byte)
+	if err != nil {
+		return protocol.Range{}, err
+	}
+	e := span.NewPoint(line, col, end.Byte)
+	return m.Range(span.New(uri, s, e))
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 9e9b035..4d7d411 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -130,6 +130,9 @@
 	// GoModForFile returns the URI of the go.mod file for the given URI.
 	GoModForFile(uri span.URI) span.URI
 
+	// WorkFile, if non-empty, is the go.work file for the workspace.
+	WorkFile() span.URI
+
 	// ParseWork is used to parse go.work files.
 	ParseWork(ctx context.Context, fh FileHandle) (*ParsedWorkFile, error)
 
@@ -656,6 +659,7 @@
 	OptimizationDetailsError DiagnosticSource = "optimizer details"
 	UpgradeNotification      DiagnosticSource = "upgrade available"
 	TemplateError            DiagnosticSource = "template"
+	WorkFileError            DiagnosticSource = "go.work file"
 )
 
 func AnalyzerErrorKind(name string) DiagnosticSource {
diff --git a/internal/lsp/work/diagnostics.go b/internal/lsp/work/diagnostics.go
new file mode 100644
index 0000000..d752484
--- /dev/null
+++ b/internal/lsp/work/diagnostics.go
@@ -0,0 +1,87 @@
+// 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/tools/internal/event"
+	"golang.org/x/tools/internal/lsp/debug/tag"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
+	ctx, done := event.Start(ctx, "work.Diagnostics", tag.Snapshot.Of(snapshot.ID()))
+	defer done()
+
+	reports := map[source.VersionedFileIdentity][]*source.Diagnostic{}
+	uri := snapshot.WorkFile()
+	if uri == "" {
+		return nil, nil
+	}
+	fh, err := snapshot.GetVersionedFile(ctx, uri)
+	if err != nil {
+		return nil, err
+	}
+	reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{}
+	diagnostics, err := DiagnosticsForWork(ctx, snapshot, fh)
+	if err != nil {
+		return nil, err
+	}
+	for _, d := range diagnostics {
+		fh, err := snapshot.GetVersionedFile(ctx, d.URI)
+		if err != nil {
+			return nil, err
+		}
+		reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d)
+	}
+
+	return reports, nil
+}
+
+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
+	}
+
+	// Add diagnostic if a directory does not contain a module.
+	var diagnostics []*source.Diagnostic
+	workdir := filepath.Dir(pw.URI.Filename())
+	for _, use := range pw.File.Use {
+		modroot := filepath.FromSlash(use.Path)
+		if !filepath.IsAbs(modroot) {
+			modroot = filepath.Join(workdir, modroot)
+		}
+
+		rng, err := source.LineToRange(pw.Mapper, fh.URI(), use.Syntax.Start, use.Syntax.End)
+		if err != nil {
+			return nil, err
+		}
+
+		modfh, err := snapshot.GetFile(ctx, span.URIFromPath(filepath.Join(modroot, "go.mod")))
+		if err != nil {
+			return nil, err
+		}
+		if _, err := modfh.Read(); err != nil && os.IsNotExist(err) {
+			diagnostics = append(diagnostics, &source.Diagnostic{
+				URI:      fh.URI(),
+				Range:    rng,
+				Severity: protocol.SeverityError,
+				Source:   source.UnknownError, // Do we need a new source for this?
+				Message:  fmt.Sprintf("directory %v does not contain a module", use.Path),
+			})
+		}
+	}
+	return diagnostics, nil
+}