github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/tests/util.go (about)

     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  
     5  package tests
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"go/token"
    12  	"path/filepath"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"testing"
    17  
    18  	"github.com/powerman/golang-tools/internal/lsp/diff"
    19  	"github.com/powerman/golang-tools/internal/lsp/diff/myers"
    20  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    21  	"github.com/powerman/golang-tools/internal/lsp/source"
    22  	"github.com/powerman/golang-tools/internal/lsp/source/completion"
    23  	"github.com/powerman/golang-tools/internal/span"
    24  )
    25  
    26  // DiffLinks takes the links we got and checks if they are located within the source or a Note.
    27  // If the link is within a Note, the link is removed.
    28  // Returns an diff comment if there are differences and empty string if no diffs.
    29  func DiffLinks(mapper *protocol.ColumnMapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string {
    30  	var notePositions []token.Position
    31  	links := make(map[span.Span]string, len(wantLinks))
    32  	for _, link := range wantLinks {
    33  		links[link.Src] = link.Target
    34  		notePositions = append(notePositions, link.NotePosition)
    35  	}
    36  	for _, link := range gotLinks {
    37  		spn, err := mapper.RangeSpan(link.Range)
    38  		if err != nil {
    39  			return fmt.Sprintf("%v", err)
    40  		}
    41  		linkInNote := false
    42  		for _, notePosition := range notePositions {
    43  			// Drop the links found inside expectation notes arguments as this links are not collected by expect package.
    44  			if notePosition.Line == spn.Start().Line() &&
    45  				notePosition.Column <= spn.Start().Column() {
    46  				delete(links, spn)
    47  				linkInNote = true
    48  			}
    49  		}
    50  		if linkInNote {
    51  			continue
    52  		}
    53  		if target, ok := links[spn]; ok {
    54  			delete(links, spn)
    55  			if target != link.Target {
    56  				return fmt.Sprintf("for %v want %v, got %v\n", spn, target, link.Target)
    57  			}
    58  		} else {
    59  			return fmt.Sprintf("unexpected link %v:%v\n", spn, link.Target)
    60  		}
    61  	}
    62  	for spn, target := range links {
    63  		return fmt.Sprintf("missing link %v:%v\n", spn, target)
    64  	}
    65  	return ""
    66  }
    67  
    68  // DiffSymbols prints the diff between expected and actual symbols test results.
    69  func DiffSymbols(t *testing.T, uri span.URI, want, got []protocol.DocumentSymbol) string {
    70  	sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
    71  	sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name })
    72  	if len(got) != len(want) {
    73  		return summarizeSymbols(-1, want, got, "different lengths got %v want %v", len(got), len(want))
    74  	}
    75  	for i, w := range want {
    76  		g := got[i]
    77  		if w.Name != g.Name {
    78  			return summarizeSymbols(i, want, got, "incorrect name got %v want %v", g.Name, w.Name)
    79  		}
    80  		if w.Kind != g.Kind {
    81  			return summarizeSymbols(i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind)
    82  		}
    83  		if protocol.CompareRange(w.SelectionRange, g.SelectionRange) != 0 {
    84  			return summarizeSymbols(i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange)
    85  		}
    86  		if msg := DiffSymbols(t, uri, w.Children, g.Children); msg != "" {
    87  			return fmt.Sprintf("children of %s: %s", w.Name, msg)
    88  		}
    89  	}
    90  	return ""
    91  }
    92  
    93  func summarizeSymbols(i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
    94  	msg := &bytes.Buffer{}
    95  	fmt.Fprint(msg, "document symbols failed")
    96  	if i >= 0 {
    97  		fmt.Fprintf(msg, " at %d", i)
    98  	}
    99  	fmt.Fprint(msg, " because of ")
   100  	fmt.Fprintf(msg, reason, args...)
   101  	fmt.Fprint(msg, ":\nexpected:\n")
   102  	for _, s := range want {
   103  		fmt.Fprintf(msg, "  %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
   104  	}
   105  	fmt.Fprintf(msg, "got:\n")
   106  	for _, s := range got {
   107  		fmt.Fprintf(msg, "  %v %v %v\n", s.Name, s.Kind, s.SelectionRange)
   108  	}
   109  	return msg.String()
   110  }
   111  
   112  // DiffDiagnostics prints the diff between expected and actual diagnostics test
   113  // results.
   114  func DiffDiagnostics(uri span.URI, want, got []*source.Diagnostic) string {
   115  	source.SortDiagnostics(want)
   116  	source.SortDiagnostics(got)
   117  
   118  	if len(got) != len(want) {
   119  		return summarizeDiagnostics(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
   120  	}
   121  	for i, w := range want {
   122  		g := got[i]
   123  		if w.Message != g.Message {
   124  			return summarizeDiagnostics(i, uri, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
   125  		}
   126  		if w.Severity != g.Severity {
   127  			return summarizeDiagnostics(i, uri, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
   128  		}
   129  		if w.Source != g.Source {
   130  			return summarizeDiagnostics(i, uri, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
   131  		}
   132  		if !rangeOverlaps(g.Range, w.Range) {
   133  			return summarizeDiagnostics(i, uri, want, got, "range %v does not overlap %v", g.Range, w.Range)
   134  		}
   135  	}
   136  	return ""
   137  }
   138  
   139  // rangeOverlaps reports whether r1 and r2 overlap.
   140  func rangeOverlaps(r1, r2 protocol.Range) bool {
   141  	if inRange(r2.Start, r1) || inRange(r1.Start, r2) {
   142  		return true
   143  	}
   144  	return false
   145  }
   146  
   147  // inRange reports whether p is contained within [r.Start, r.End), or if p ==
   148  // r.Start == r.End (special handling for the case where the range is a single
   149  // point).
   150  func inRange(p protocol.Position, r protocol.Range) bool {
   151  	if protocol.IsPoint(r) {
   152  		return protocol.ComparePosition(r.Start, p) == 0
   153  	}
   154  	if protocol.ComparePosition(r.Start, p) <= 0 && protocol.ComparePosition(p, r.End) < 0 {
   155  		return true
   156  	}
   157  	return false
   158  }
   159  
   160  func summarizeDiagnostics(i int, uri span.URI, want, got []*source.Diagnostic, reason string, args ...interface{}) string {
   161  	msg := &bytes.Buffer{}
   162  	fmt.Fprint(msg, "diagnostics failed")
   163  	if i >= 0 {
   164  		fmt.Fprintf(msg, " at %d", i)
   165  	}
   166  	fmt.Fprint(msg, " because of ")
   167  	fmt.Fprintf(msg, reason, args...)
   168  	fmt.Fprint(msg, ":\nexpected:\n")
   169  	for _, d := range want {
   170  		fmt.Fprintf(msg, "  %s:%v: %s\n", uri, d.Range, d.Message)
   171  	}
   172  	fmt.Fprintf(msg, "got:\n")
   173  	for _, d := range got {
   174  		fmt.Fprintf(msg, "  %s:%v: %s\n", uri, d.Range, d.Message)
   175  	}
   176  	return msg.String()
   177  }
   178  
   179  func DiffCodeLens(uri span.URI, want, got []protocol.CodeLens) string {
   180  	sortCodeLens(want)
   181  	sortCodeLens(got)
   182  
   183  	if len(got) != len(want) {
   184  		return summarizeCodeLens(-1, uri, want, got, "different lengths got %v want %v", len(got), len(want))
   185  	}
   186  	for i, w := range want {
   187  		g := got[i]
   188  		if w.Command.Command != g.Command.Command {
   189  			return summarizeCodeLens(i, uri, want, got, "incorrect Command Name got %v want %v", g.Command.Command, w.Command.Command)
   190  		}
   191  		if w.Command.Title != g.Command.Title {
   192  			return summarizeCodeLens(i, uri, want, got, "incorrect Command Title got %v want %v", g.Command.Title, w.Command.Title)
   193  		}
   194  		if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
   195  			return summarizeCodeLens(i, uri, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
   196  		}
   197  		if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the codelens returns a zero-length range.
   198  			if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
   199  				return summarizeCodeLens(i, uri, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
   200  			}
   201  		}
   202  	}
   203  	return ""
   204  }
   205  
   206  func sortCodeLens(c []protocol.CodeLens) {
   207  	sort.Slice(c, func(i int, j int) bool {
   208  		if r := protocol.CompareRange(c[i].Range, c[j].Range); r != 0 {
   209  			return r < 0
   210  		}
   211  		if c[i].Command.Command < c[j].Command.Command {
   212  			return true
   213  		} else if c[i].Command.Command == c[j].Command.Command {
   214  			return c[i].Command.Title < c[j].Command.Title
   215  		} else {
   216  			return false
   217  		}
   218  	})
   219  }
   220  
   221  func summarizeCodeLens(i int, uri span.URI, want, got []protocol.CodeLens, reason string, args ...interface{}) string {
   222  	msg := &bytes.Buffer{}
   223  	fmt.Fprint(msg, "codelens failed")
   224  	if i >= 0 {
   225  		fmt.Fprintf(msg, " at %d", i)
   226  	}
   227  	fmt.Fprint(msg, " because of ")
   228  	fmt.Fprintf(msg, reason, args...)
   229  	fmt.Fprint(msg, ":\nexpected:\n")
   230  	for _, d := range want {
   231  		fmt.Fprintf(msg, "  %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
   232  	}
   233  	fmt.Fprintf(msg, "got:\n")
   234  	for _, d := range got {
   235  		fmt.Fprintf(msg, "  %s:%v: %s | %s\n", uri, d.Range, d.Command.Command, d.Command.Title)
   236  	}
   237  	return msg.String()
   238  }
   239  
   240  func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) (string, error) {
   241  	decorate := func(f string, args ...interface{}) string {
   242  		return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...))
   243  	}
   244  	if len(got.Signatures) != 1 {
   245  		return decorate("wanted 1 signature, got %d", len(got.Signatures)), nil
   246  	}
   247  	if got.ActiveSignature != 0 {
   248  		return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)), nil
   249  	}
   250  	if want.ActiveParameter != got.ActiveParameter {
   251  		return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)), nil
   252  	}
   253  	g := got.Signatures[0]
   254  	w := want.Signatures[0]
   255  	if NormalizeAny(w.Label) != NormalizeAny(g.Label) {
   256  		wLabel := w.Label + "\n"
   257  		d, err := myers.ComputeEdits("", wLabel, g.Label+"\n")
   258  		if err != nil {
   259  			return "", err
   260  		}
   261  		return decorate("mismatched labels:\n%q", diff.ToUnified("want", "got", wLabel, d)), err
   262  	}
   263  	var paramParts []string
   264  	for _, p := range g.Parameters {
   265  		paramParts = append(paramParts, p.Label)
   266  	}
   267  	paramsStr := strings.Join(paramParts, ", ")
   268  	if !strings.Contains(g.Label, paramsStr) {
   269  		return decorate("expected signature %q to contain params %q", g.Label, paramsStr), nil
   270  	}
   271  	return "", nil
   272  }
   273  
   274  // NormalizeAny replaces occurrences of interface{} in input with any.
   275  //
   276  // In Go 1.18, standard library functions were changed to use the 'any'
   277  // alias in place of interface{}, which affects their type string.
   278  func NormalizeAny(input string) string {
   279  	return strings.ReplaceAll(input, "interface{}", "any")
   280  }
   281  
   282  // DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies
   283  func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string {
   284  	expected := make(map[protocol.Location]bool)
   285  	for _, call := range expectedCalls {
   286  		expected[protocol.Location{URI: call.URI, Range: call.Range}] = true
   287  	}
   288  
   289  	got := make(map[protocol.Location]bool)
   290  	for _, call := range gotCalls {
   291  		got[protocol.Location{URI: call.URI, Range: call.Range}] = true
   292  	}
   293  	if len(got) != len(expected) {
   294  		return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got))
   295  	}
   296  	for spn := range got {
   297  		if !expected[spn] {
   298  			return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got)
   299  		}
   300  	}
   301  	return ""
   302  }
   303  
   304  func ToProtocolCompletionItems(items []completion.CompletionItem) []protocol.CompletionItem {
   305  	var result []protocol.CompletionItem
   306  	for _, item := range items {
   307  		result = append(result, ToProtocolCompletionItem(item))
   308  	}
   309  	return result
   310  }
   311  
   312  func ToProtocolCompletionItem(item completion.CompletionItem) protocol.CompletionItem {
   313  	pItem := protocol.CompletionItem{
   314  		Label:         item.Label,
   315  		Kind:          item.Kind,
   316  		Detail:        item.Detail,
   317  		Documentation: item.Documentation,
   318  		InsertText:    item.InsertText,
   319  		TextEdit: &protocol.TextEdit{
   320  			NewText: item.Snippet(),
   321  		},
   322  		// Negate score so best score has lowest sort text like real API.
   323  		SortText: fmt.Sprint(-item.Score),
   324  	}
   325  	if pItem.InsertText == "" {
   326  		pItem.InsertText = pItem.Label
   327  	}
   328  	return pItem
   329  }
   330  
   331  func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem {
   332  	var (
   333  		got          []protocol.CompletionItem
   334  		wantBuiltins = strings.Contains(string(src.URI()), "builtins")
   335  		wantKeywords = strings.Contains(string(src.URI()), "keywords")
   336  	)
   337  	for _, item := range items {
   338  		if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) {
   339  			continue
   340  		}
   341  
   342  		if !wantKeywords && token.Lookup(item.Label).IsKeyword() {
   343  			continue
   344  		}
   345  
   346  		got = append(got, item)
   347  	}
   348  	return got
   349  }
   350  
   351  func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
   352  	if detail == "" && kind == protocol.ClassCompletion {
   353  		return true
   354  	}
   355  	// Remaining builtin constants, variables, interfaces, and functions.
   356  	trimmed := label
   357  	if i := strings.Index(trimmed, "("); i >= 0 {
   358  		trimmed = trimmed[:i]
   359  	}
   360  	switch trimmed {
   361  	case "append", "cap", "close", "complex", "copy", "delete",
   362  		"error", "false", "imag", "iota", "len", "make", "new",
   363  		"nil", "panic", "print", "println", "real", "recover", "true":
   364  		return true
   365  	}
   366  	return false
   367  }
   368  
   369  func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string {
   370  	var (
   371  		matchedIdxs []int
   372  		lastGotIdx  int
   373  		lastGotSort float64
   374  		inOrder     = true
   375  		errorMsg    = "completions out of order"
   376  	)
   377  	for _, w := range want {
   378  		var found bool
   379  		for i, g := range got {
   380  			if w.Label == g.Label && NormalizeAny(w.Detail) == NormalizeAny(g.Detail) && w.Kind == g.Kind {
   381  				matchedIdxs = append(matchedIdxs, i)
   382  				found = true
   383  
   384  				if i < lastGotIdx {
   385  					inOrder = false
   386  				}
   387  				lastGotIdx = i
   388  
   389  				sort, _ := strconv.ParseFloat(g.SortText, 64)
   390  				if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort {
   391  					inOrder = false
   392  					errorMsg = "candidate scores not strictly decreasing"
   393  				}
   394  				lastGotSort = sort
   395  
   396  				break
   397  			}
   398  		}
   399  		if !found {
   400  			return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
   401  		}
   402  	}
   403  
   404  	sort.Ints(matchedIdxs)
   405  	matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
   406  	for _, idx := range matchedIdxs {
   407  		matched = append(matched, got[idx])
   408  	}
   409  
   410  	if !inOrder {
   411  		return summarizeCompletionItems(-1, want, matched, errorMsg)
   412  	}
   413  
   414  	return ""
   415  }
   416  
   417  func DiffSnippets(want string, got *protocol.CompletionItem) string {
   418  	if want == "" {
   419  		if got != nil {
   420  			x := got.TextEdit
   421  			return fmt.Sprintf("expected no snippet but got %s", x.NewText)
   422  		}
   423  	} else {
   424  		if got == nil {
   425  			return fmt.Sprintf("couldn't find completion matching %q", want)
   426  		}
   427  		x := got.TextEdit
   428  		if want != x.NewText {
   429  			return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText)
   430  		}
   431  	}
   432  	return ""
   433  }
   434  
   435  func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *protocol.CompletionItem {
   436  	for _, item := range list {
   437  		if item.Label == want.Label {
   438  			return &item
   439  		}
   440  	}
   441  	return nil
   442  }
   443  
   444  // DiffCompletionItems prints the diff between expected and actual completion
   445  // test results.
   446  func DiffCompletionItems(want, got []protocol.CompletionItem) string {
   447  	if len(got) != len(want) {
   448  		return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
   449  	}
   450  	for i, w := range want {
   451  		g := got[i]
   452  		if w.Label != g.Label {
   453  			return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
   454  		}
   455  		if NormalizeAny(w.Detail) != NormalizeAny(g.Detail) {
   456  			return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
   457  		}
   458  		if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
   459  			if w.Documentation != g.Documentation {
   460  				return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
   461  			}
   462  		}
   463  		if w.Kind != g.Kind {
   464  			return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
   465  		}
   466  	}
   467  	return ""
   468  }
   469  
   470  func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
   471  	msg := &bytes.Buffer{}
   472  	fmt.Fprint(msg, "completion failed")
   473  	if i >= 0 {
   474  		fmt.Fprintf(msg, " at %d", i)
   475  	}
   476  	fmt.Fprint(msg, " because of ")
   477  	fmt.Fprintf(msg, reason, args...)
   478  	fmt.Fprint(msg, ":\nexpected:\n")
   479  	for _, d := range want {
   480  		fmt.Fprintf(msg, "  %v\n", d)
   481  	}
   482  	fmt.Fprintf(msg, "got:\n")
   483  	for _, d := range got {
   484  		fmt.Fprintf(msg, "  %v\n", d)
   485  	}
   486  	return msg.String()
   487  }
   488  
   489  func EnableAllAnalyzers(view source.View, opts *source.Options) {
   490  	if opts.Analyses == nil {
   491  		opts.Analyses = make(map[string]bool)
   492  	}
   493  	for _, a := range opts.DefaultAnalyzers {
   494  		if !a.IsEnabled(view) {
   495  			opts.Analyses[a.Analyzer.Name] = true
   496  		}
   497  	}
   498  	for _, a := range opts.TypeErrorAnalyzers {
   499  		if !a.IsEnabled(view) {
   500  			opts.Analyses[a.Analyzer.Name] = true
   501  		}
   502  	}
   503  	for _, a := range opts.ConvenienceAnalyzers {
   504  		if !a.IsEnabled(view) {
   505  			opts.Analyses[a.Analyzer.Name] = true
   506  		}
   507  	}
   508  	for _, a := range opts.StaticcheckAnalyzers {
   509  		if !a.IsEnabled(view) {
   510  			opts.Analyses[a.Analyzer.Name] = true
   511  		}
   512  	}
   513  }
   514  
   515  func WorkspaceSymbolsString(ctx context.Context, data *Data, queryURI span.URI, symbols []protocol.SymbolInformation) (string, error) {
   516  	queryDir := filepath.Dir(queryURI.Filename())
   517  	var filtered []string
   518  	for _, s := range symbols {
   519  		uri := s.Location.URI.SpanURI()
   520  		dir := filepath.Dir(uri.Filename())
   521  		if !source.InDir(queryDir, dir) { // assume queries always issue from higher directories
   522  			continue
   523  		}
   524  		m, err := data.Mapper(uri)
   525  		if err != nil {
   526  			return "", err
   527  		}
   528  		spn, err := m.Span(s.Location)
   529  		if err != nil {
   530  			return "", err
   531  		}
   532  		filtered = append(filtered, fmt.Sprintf("%s %s %s", spn, s.Name, s.Kind))
   533  	}
   534  	sort.Strings(filtered)
   535  	return strings.Join(filtered, "\n") + "\n", nil
   536  }
   537  
   538  func WorkspaceSymbolsTestTypeToMatcher(typ WorkspaceSymbolsTestType) source.SymbolMatcher {
   539  	switch typ {
   540  	case WorkspaceSymbolsFuzzy:
   541  		return source.SymbolFuzzy
   542  	case WorkspaceSymbolsCaseSensitive:
   543  		return source.SymbolCaseSensitive
   544  	default:
   545  		return source.SymbolCaseInsensitive
   546  	}
   547  }
   548  
   549  func Diff(t *testing.T, want, got string) string {
   550  	if want == got {
   551  		return ""
   552  	}
   553  	// Add newlines to avoid newline messages in diff.
   554  	want += "\n"
   555  	got += "\n"
   556  	d, err := myers.ComputeEdits("", want, got)
   557  	if err != nil {
   558  		t.Fatal(err)
   559  	}
   560  	return fmt.Sprintf("%q", diff.ToUnified("want", "got", want, d))
   561  }
   562  
   563  // StripSubscripts removes type parameter id subscripts.
   564  //
   565  // TODO(rfindley): remove this function once subscripts are removed from the
   566  // type parameter type string.
   567  func StripSubscripts(s string) string {
   568  	var runes []rune
   569  	for _, r := range s {
   570  		// For debugging/uniqueness purposes, TypeString on a type parameter adds a
   571  		// subscript corresponding to the type parameter's unique id. This is going
   572  		// to be removed, but in the meantime we skip the subscript runes to get a
   573  		// deterministic output.
   574  		if '₀' <= r && r < '₀'+10 {
   575  			continue // trim type parameter subscripts
   576  		}
   577  		runes = append(runes, r)
   578  	}
   579  	return string(runes)
   580  }