| // 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) |
| } |
| }) |
| } |