github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/mod/diagnostics.go (about)

     1  // Copyright 2019 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  
     5  // Package mod provides core features related to go.mod file
     6  // handling for use by Go editors and tools.
     7  package mod
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"regexp"
    13  	"strings"
    14  	"unicode"
    15  
    16  	"golang.org/x/mod/modfile"
    17  	"golang.org/x/mod/module"
    18  	"github.com/april1989/origin-go-tools/internal/event"
    19  	"github.com/april1989/origin-go-tools/internal/lsp/debug/tag"
    20  	"github.com/april1989/origin-go-tools/internal/lsp/protocol"
    21  	"github.com/april1989/origin-go-tools/internal/lsp/source"
    22  	"github.com/april1989/origin-go-tools/internal/span"
    23  )
    24  
    25  func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
    26  	uri := snapshot.View().ModFile()
    27  	if uri == "" {
    28  		return nil, nil
    29  	}
    30  
    31  	ctx, done := event.Start(ctx, "mod.Diagnostics", tag.URI.Of(uri))
    32  	defer done()
    33  
    34  	fh, err := snapshot.GetFile(ctx, uri)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  	tidied, err := snapshot.ModTidy(ctx, fh)
    39  	if err == source.ErrTmpModfileUnsupported {
    40  		return nil, nil
    41  	}
    42  	reports := map[source.VersionedFileIdentity][]*source.Diagnostic{
    43  		fh.VersionedFileIdentity(): {},
    44  	}
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	for _, e := range tidied.Errors {
    49  		diag := &source.Diagnostic{
    50  			Message: e.Message,
    51  			Range:   e.Range,
    52  			Source:  e.Category,
    53  		}
    54  		if e.Category == "syntax" {
    55  			diag.Severity = protocol.SeverityError
    56  		} else {
    57  			diag.Severity = protocol.SeverityWarning
    58  		}
    59  		fh, err := snapshot.GetFile(ctx, e.URI)
    60  		if err != nil {
    61  			return nil, err
    62  		}
    63  		reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], diag)
    64  	}
    65  	return reports, nil
    66  }
    67  
    68  var moduleAtVersionRe = regexp.MustCompile(`^(?P<module>.*)@(?P<version>.*)$`)
    69  
    70  // ExtractGoCommandError tries to parse errors that come from the go command
    71  // and shape them into go.mod diagnostics.
    72  func ExtractGoCommandError(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loadErr error) (*source.Diagnostic, error) {
    73  	// We try to match module versions in error messages. Some examples:
    74  	//
    75  	//  example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory
    76  	//  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
    77  	//  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
    78  	//
    79  	// We split on colons and whitespace, and attempt to match on something
    80  	// that matches module@version. If we're able to find a match, we try to
    81  	// find anything that matches it in the go.mod file.
    82  	var v module.Version
    83  	fields := strings.FieldsFunc(loadErr.Error(), func(r rune) bool {
    84  		return unicode.IsSpace(r) || r == ':'
    85  	})
    86  	for _, s := range fields {
    87  		s = strings.TrimSpace(s)
    88  		match := moduleAtVersionRe.FindStringSubmatch(s)
    89  		if match == nil || len(match) < 3 {
    90  			continue
    91  		}
    92  		v.Path = match[1]
    93  		v.Version = match[2]
    94  		if err := module.Check(v.Path, v.Version); err == nil {
    95  			break
    96  		}
    97  	}
    98  	pm, err := snapshot.ParseMod(ctx, fh)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	toDiagnostic := func(line *modfile.Line) (*source.Diagnostic, error) {
   103  		rng, err := rangeFromPositions(fh.URI(), pm.Mapper, line.Start, line.End)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  		return &source.Diagnostic{
   108  			Message:  loadErr.Error(),
   109  			Range:    rng,
   110  			Severity: protocol.SeverityError,
   111  		}, nil
   112  	}
   113  	// Check if there are any require, exclude, or replace statements that
   114  	// match this module version.
   115  	for _, req := range pm.File.Require {
   116  		if req.Mod != v {
   117  			continue
   118  		}
   119  		return toDiagnostic(req.Syntax)
   120  	}
   121  	for _, ex := range pm.File.Exclude {
   122  		if ex.Mod != v {
   123  			continue
   124  		}
   125  		return toDiagnostic(ex.Syntax)
   126  	}
   127  	for _, rep := range pm.File.Replace {
   128  		if rep.New != v && rep.Old != v {
   129  			continue
   130  		}
   131  		return toDiagnostic(rep.Syntax)
   132  	}
   133  	// No match for the module path was found in the go.mod file.
   134  	// Show the error on the module declaration, if one exists.
   135  	if pm.File.Module == nil {
   136  		return nil, fmt.Errorf("no module declaration in %s", fh.URI())
   137  	}
   138  	return toDiagnostic(pm.File.Module.Syntax)
   139  }
   140  
   141  func rangeFromPositions(uri span.URI, m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
   142  	toPoint := func(offset int) (span.Point, error) {
   143  		l, c, err := m.Converter.ToPosition(offset)
   144  		if err != nil {
   145  			return span.Point{}, err
   146  		}
   147  		return span.NewPoint(l, c, offset), nil
   148  	}
   149  	start, err := toPoint(s.Byte)
   150  	if err != nil {
   151  		return protocol.Range{}, err
   152  	}
   153  	end, err := toPoint(e.Byte)
   154  	if err != nil {
   155  		return protocol.Range{}, err
   156  	}
   157  	return m.Range(span.New(uri, start, end))
   158  }