diff --git a/src/go/types/api.go b/src/go/types/api.go index fb585fdd0c..510a0e736c 100644 --- a/src/go/types/api.go +++ b/src/go/types/api.go @@ -41,7 +41,8 @@ import ( type Error struct { Fset *token.FileSet // file set for interpretation of Pos Pos token.Pos // error position - Msg string // error message + Msg string // default error message, user-friendly + Full string // full error message, for debugging (may contain internal details) Soft bool // if set, error is "soft" } @@ -51,6 +52,13 @@ func (err Error) Error() string { return fmt.Sprintf("%s: %s", err.Fset.Position(err.Pos), err.Msg) } +// FullError returns an error string like Error, buy it may contain +// type-checker internal details such as subscript indices for type +// parameters and more. Useful for debugging. +func (err Error) FullError() string { + return fmt.Sprintf("%s: %s", err.Fset.Position(err.Pos), err.Full) +} + // An Importer resolves import paths to Packages. // // CAUTION: This interface does not support the import of locally diff --git a/src/go/types/errors.go b/src/go/types/errors.go index 2bb92cc7a0..9ed8b8cc6b 100644 --- a/src/go/types/errors.go +++ b/src/go/types/errors.go @@ -12,6 +12,8 @@ import ( "go/token" "strconv" "strings" + "unicode" + "unicode/utf8" ) func assert(p bool) { @@ -82,7 +84,7 @@ func (check *Checker) err(pos token.Pos, msg string, soft bool) { return } - err := Error{check.fset, pos, msg, soft} + err := Error{check.fset, pos, cleanMsg(msg), msg, soft} if check.firstErr == nil { check.firstErr = err } @@ -121,3 +123,54 @@ func (check *Checker) invalidArg(pos token.Pos, format string, args ...interface func (check *Checker) invalidOp(pos token.Pos, format string, args ...interface{}) { check.errorf(pos, "invalid operation: "+format, args...) } + +// cleanMsg removes subscripts and replaces <>'s in instantiated type names with ()'s. +func cleanMsg(s string) string { + var b strings.Builder + var p rune // previous rune + n := 0 // nesting level + copy := false // indicates that we need a copy + for i := 0; ; { + r, w := utf8.DecodeRuneInString(s[i:]) + i += w + if r == utf8.RuneError { + if w == 0 { + break // we're done + } + if w == 1 { + continue // ignore (this should never happen) + } + } + + // strip subscript digits + if '₀' <= r && r < '₀'+10 { // '₀' == U+2080 + copy = true + continue + } + + // replace <>'s in instantiated type names by ()'s + // (use previous rune p and nesting level n for more + // accurate replacement) + if r == '<' && isIdentChar(p) { + n++ + copy = true + r = '(' + } else if r == '>' && n > 0 { + n-- + copy = true + r = ')' + } + + b.WriteRune(r) + p = r + } + + if copy { + return b.String() + } + return s +} + +func isIdentChar(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' +} diff --git a/src/go/types/errors_test.go b/src/go/types/errors_test.go new file mode 100644 index 0000000000..7c5619cf52 --- /dev/null +++ b/src/go/types/errors_test.go @@ -0,0 +1,27 @@ +// Copyright 2020 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 types + +import "testing" + +func TestCleanMsg(t *testing.T) { + for _, test := range []struct { + in, want string + }{ + {"", ""}, + {" ", " "}, + {"foo", "foo"}, + {"foo", "foo(T)"}, + {"foo ", "foo "}, + {"foo << bar", "foo << bar"}, + {"foo₀", "foo"}, + {"foo", "foo(T)"}, + } { + got := cleanMsg(test.in) + if got != test.want { + t.Errorf("%q: got %q; want %q", test.in, got, test.want) + } + } +}