blob: 6069fe467fcaf70d5038deb67151d3fbb31e3723 [file]
// Copyright 2021 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 misc
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/protocol"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
)
func TestBadURICrash_VSCodeIssue1498(t *testing.T) {
const src = `
-- go.mod --
module example.com
go 1.12
-- main.go --
package main
func main() {}
`
WithOptions(
Modes(Default),
).Run(t, src, func(t *testing.T, env *Env) {
params := &protocol.SemanticTokensParams{}
const badURI = "http://foo"
params.TextDocument.URI = badURI
// This call panicked in the past: golang/vscode-go#1498.
_, err := env.Editor.Server.SemanticTokensFull(env.Ctx, params)
// Requests to an invalid URI scheme now result in an LSP error.
got := fmt.Sprint(err)
want := `DocumentURI scheme is not 'file': http://foo`
if !strings.Contains(got, want) {
t.Errorf("SemanticTokensFull error is %v, want substring %q", got, want)
}
})
}
// fix bug involving type parameters and regular parameters
// (golang/vscode-go#2527)
func TestSemantic_2527(t *testing.T) {
// these are the expected types of identifiers in text order
want := []fake.SemanticToken{
{Token: "package", TokenType: "keyword"},
{Token: "foo", TokenType: "namespace"},
{Token: "// comment", TokenType: "comment"},
{Token: "func", TokenType: "keyword"},
{Token: "Add", TokenType: "function", Mod: "definition signature"},
{Token: "T", TokenType: "typeParameter", Mod: "definition"},
{Token: "int", TokenType: "type", Mod: "defaultLibrary number"},
{Token: "target", TokenType: "parameter", Mod: "definition"},
{Token: "T", TokenType: "typeParameter"},
{Token: "l", TokenType: "parameter", Mod: "definition slice"},
{Token: "T", TokenType: "typeParameter"},
{Token: "T", TokenType: "typeParameter"},
{Token: "return", TokenType: "keyword"},
{Token: "append", TokenType: "function", Mod: "defaultLibrary"},
{Token: "l", TokenType: "parameter", Mod: "slice"},
{Token: "target", TokenType: "parameter"},
{Token: "for", TokenType: "keyword"},
{Token: "range", TokenType: "keyword"},
{Token: "l", TokenType: "parameter", Mod: "slice"},
{Token: "// test coverage", TokenType: "comment"},
{Token: "return", TokenType: "keyword"},
{Token: "nil", TokenType: "variable", Mod: "readonly defaultLibrary"},
}
src := `
-- go.mod --
module example.com
go 1.19
-- main.go --
package foo
// comment
func Add[T int](target T, l []T) []T {
return append(l, target)
for range l {} // test coverage
return nil
}
`
WithOptions(
Modes(Default),
Settings{"semanticTokens": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
env.AfterChange(
Diagnostics(env.AtRegexp("main.go", "for range")),
)
seen := env.SemanticTokensFull("main.go")
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}
// fix inconsistency in TypeParameters
// https://github.com/golang/go/issues/57619
func TestSemantic_57619(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.19
-- main.go --
package foo
type Smap[K int, V any] struct {
Store map[K]V
}
func (s *Smap[K, V]) Get(k K) (V, bool) {
v, ok := s.Store[k]
return v, ok
}
func New[K int, V any]() Smap[K, V] {
return Smap[K, V]{Store: make(map[K]V)}
}
`
WithOptions(
Modes(Default),
Settings{"semanticTokens": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
seen := env.SemanticTokensFull("main.go")
for i, s := range seen {
if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" {
t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s)
}
}
})
}
func TestSemanticGoDirectives(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.19
-- main.go --
package foo
//go:linkname now time.Now
func now()
//go:noinline
func foo() {}
// Mentioning go:noinline should not tokenize.
//go:notadirective
func bar() {}
`
want := []fake.SemanticToken{
{Token: "package", TokenType: "keyword"},
{Token: "foo", TokenType: "namespace"},
{Token: "//", TokenType: "comment"},
{Token: "go:linkname", TokenType: "namespace"},
{Token: "now time.Now", TokenType: "comment"},
{Token: "func", TokenType: "keyword"},
{Token: "now", TokenType: "function", Mod: "definition signature"},
{Token: "//", TokenType: "comment"},
{Token: "go:noinline", TokenType: "namespace"},
{Token: "func", TokenType: "keyword"},
{Token: "foo", TokenType: "function", Mod: "definition signature"},
{Token: "// Mentioning go:noinline should not tokenize.", TokenType: "comment"},
{Token: "//go:notadirective", TokenType: "comment"},
{Token: "func", TokenType: "keyword"},
{Token: "bar", TokenType: "function", Mod: "definition signature"},
}
WithOptions(
Modes(Default),
Settings{"semanticTokens": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
seen := env.SemanticTokensFull("main.go")
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}
// Make sure no zero-length tokens occur
func TestSemantic_65254(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.21
-- main.go --
package main
/* a comment with an
empty line
*/
const bad = `
src += "`foo" + `
` + "bar`"
want := []fake.SemanticToken{
{Token: "package", TokenType: "keyword"},
{Token: "main", TokenType: "namespace"},
{Token: "/* a comment with an", TokenType: "comment"},
// --- Note that the zero length line does not show up
{Token: "empty line", TokenType: "comment"},
{Token: "*/", TokenType: "comment"},
{Token: "const", TokenType: "keyword"},
{Token: "bad", TokenType: "variable", Mod: "definition readonly"},
{Token: "`foo", TokenType: "string"},
// --- Note the zero length line does not show up
{Token: "\tbar`", TokenType: "string"},
}
WithOptions(
Modes(Default),
Settings{"semanticTokens": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
seen := env.SemanticTokensFull("main.go")
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}
// Check delta-encoding of token line is sound even when the first
// token is discarded (go.dev/issue/78269).
func TestSemanticTokens_firstSuppressed(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.21
-- p.go --
// comment
package p
// imports
import _ "fmt"
func F()
`
WithOptions(
Modes(Default),
Settings{
"semanticTokens": true,
"semanticTokenTypes": map[string]any{
"keyword": false,
"namespace": false,
"comment": false,
},
},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("p.go")
seen := env.SemanticTokensFull("p.go")
want := []fake.SemanticToken{
// These tokens are suppressed by the settings above:
// {Token: "// comment", TokenType: "comment"},
// {Token: "package", TokenType: "keyword"},
// {Token: "p", TokenType: "namespace"},
// {Token: "// imports", TokenType: "comment"},
// {Token: "import", TokenType: "keyword"},
// {Token: "func", TokenType: "keyword"},
// This first token must be reported at "F":
{Token: "F", TokenType: "function", Mod: "definition signature"},
}
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}
// This test exercises the UTF-16 logic in Editor.interpretTokens.
func TestSemanticTokens_multiline(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.21
-- p.go --
package p
` + "const _ = `δΈ–\nπŸ˜ƒ`"
WithOptions(
Modes(Default),
Settings{
"semanticTokens": true,
"semanticTokenTypes": map[string]any{
"package": false,
"keyword": false,
"namespace": false,
"variable": false,
"string": true,
},
},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("p.go")
seen := env.SemanticTokensFull("p.go")
want := []fake.SemanticToken{
{Token: "`δΈ–", TokenType: "string"},
{Token: "πŸ˜ƒ`", TokenType: "string"},
}
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}
// This sets client capability fields:
// - `textDocument.semanticTokens.tokenTypes`
// - `textDocument.semanticTokens.tokenModifiers`
func tokenClientCapabilities(t *testing.T, tokenTypes, tokenMods []string) RunOption {
t.Helper()
typeString, err := json.Marshal(tokenTypes)
if err != nil {
t.Fatalf("accepted token list failed to marshal: %v", err)
}
modString, err := json.Marshal(tokenMods)
if err != nil {
t.Fatalf("accepted mod list failed to marshal: %v", err)
}
// This will only update the specific client capabilities set.
return CapabilitiesJSON(fmt.Appendf(nil,
`{"textDocument": {"semanticTokens": {"tokenTypes": %s, "tokenModifiers": %s}}}`,
typeString, modString))
}
// This tests the Client Capabilities:
// - `textDocument.semanticTokens.tokenTypes`
// - `textDocument.semanticTokens.tokenModifiers`
//
// These two client capabilities determine the maximum set of types and mods
// the server can send back to the client. So the intersection of these lists
// with the lists of what the server supports is what gopls can return.
func TestSemanticTokenClientCapabilitiesTypeAndModifier(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.19
-- main.go --
package foo
// Comment
type A struct {}
`
tcs := []struct {
name string
tokenTypes []string
tokenModifiers []string
want []fake.SemanticToken
}{{
name: "base case",
tokenTypes: []string{"comment", "keyword", "namespace", "type"},
tokenModifiers: []string{"definition", "struct"},
want: []fake.SemanticToken{
{Token: "package", TokenType: "keyword"},
{Token: "foo", TokenType: "namespace"},
{Token: "// Comment", TokenType: "comment"},
{Token: "type", TokenType: "keyword"},
{Token: "A", TokenType: "type", Mod: "definition struct"},
{Token: "struct", TokenType: "keyword"},
},
}, {
name: "support subset of token types",
tokenTypes: []string{"comment", "type"},
tokenModifiers: []string{"definition", "struct"},
want: []fake.SemanticToken{
{Token: "// Comment", TokenType: "comment"},
{Token: "A", TokenType: "type", Mod: "definition struct"},
},
}, {
name: "support subset of modifiers",
tokenTypes: []string{"comment", "keyword", "namespace", "type"},
tokenModifiers: []string{"definition"},
want: []fake.SemanticToken{
{Token: "package", TokenType: "keyword"},
{Token: "foo", TokenType: "namespace"},
{Token: "// Comment", TokenType: "comment"},
{Token: "type", TokenType: "keyword"},
// "struct" modifier no longer supported
{Token: "A", TokenType: "type", Mod: "definition"},
{Token: "struct", TokenType: "keyword"},
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
WithOptions(
Modes(Default),
tokenClientCapabilities(t, tc.tokenTypes, tc.tokenModifiers),
Settings{"semanticTokens": true},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
seen := env.SemanticTokensFull("main.go")
if x := cmp.Diff(tc.want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
})
}
}
// This test verifies that UI Setting's `semanticTokens` map only further
// restricts the client capabilities `tokenTypes` list.
//
// TODO(aputman): Consider moving to marker test.
func TestSemanticTokenTypesClientCapabilitiesWithUISettings(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.19
-- main.go --
package foo
// Comment
type A struct {}
`
// What the client capability token types are.
clientTokenTypes := []string{"comment", "keyword", "type"}
// The UI Settings token types map
uiTokenTypes := map[string]bool{
// Since "namespace" isn't in the clientTokenTypes list, this
// won't do anything.
"namespace": true,
"type": true,
// This will disable the keyword tokentypes even though the client
// supports them.
"keyword": false,
// Since "comment" is left out, it will be supported.
}
clientTokenModifiers := []string{"definition", "struct"}
// Only comment and type are left
want := []fake.SemanticToken{
{Token: "// Comment", TokenType: "comment"},
{Token: "A", TokenType: "type", Mod: "definition struct"},
}
WithOptions(
Modes(Default),
tokenClientCapabilities(t, clientTokenTypes, clientTokenModifiers),
Settings{"semanticTokens": true, "semanticTokenTypes": uiTokenTypes},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
seen := env.SemanticTokensFull("main.go")
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}
// This test verifies that UI Setting's semanticTokenModifiers map only
// further restricts the client capabilities tokenModifiers list.
//
// TODO(aputman): Consider moving to marker test.
func TestSemanticTokenModifiersClientCapabilitiesWithUISettings(t *testing.T) {
src := `
-- go.mod --
module example.com
go 1.19
-- main.go --
package foo
type A struct {}
func B(l []string) {}
`
// Only these token types are returned.
clientTokenTypes := []string{"comment", "definition", "function", "parameter", "type"}
// What the client capability token modifiers are.
clientTokenModifiers := []string{"definition", "signature", "struct"}
// The UI Settings token mods map
uiTokenMods := map[string]bool{
// Since "slice" isn't in the clientTokenModifiers list, this won't do anything.
"slice": true,
"signature": true,
// This will disable the definition modifiers even though the client supports them.
"definition": false,
// Since "struct" is left out, it will be supported.
}
// Only signature and struct modifiers are left
want := []fake.SemanticToken{
{Token: "A", TokenType: "type", Mod: "struct"}, // "definition" no longer returned
{Token: "B", TokenType: "function", Mod: "signature"}, // "definition" no longer returned
{Token: "l", TokenType: "parameter", Mod: ""}, // "slice" no longer returned
{Token: "string", TokenType: "type", Mod: ""},
}
WithOptions(
Modes(Default),
tokenClientCapabilities(t, clientTokenTypes, clientTokenModifiers),
Settings{"semanticTokens": true, "semanticTokenModifiers": uiTokenMods},
).Run(t, src, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
seen := env.SemanticTokensFull("main.go")
if x := cmp.Diff(want, seen); x != "" {
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
}
})
}