Skip to content

Commit b82788e

Browse files
vikblomgopherbot
authored andcommitted
gopls/internal/lsp: add semantic highlighting for go: directives
Highlight known go:directives in their surrounding comment. Unknown or misspelled directives are skipped to signal to the end user that something is off. Closes golang/go#63538 Change-Id: Idf926106600b734ce2076366798acef0051181d6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/536915 Auto-Submit: Robert Findley <rfindley@google.com> Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
1 parent 6da1917 commit b82788e

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

gopls/internal/lsp/semantic.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ func (e *encoded) semantics() {
143143
}
144144
for _, cg := range f.Comments {
145145
for _, c := range cg.List {
146+
if strings.HasPrefix(c.Text, "//go:") {
147+
e.godirective(c)
148+
continue
149+
}
146150
if !strings.Contains(c.Text, "\n") {
147151
e.token(c.Pos(), len(c.Text), tokComment, nil)
148152
continue
@@ -997,3 +1001,52 @@ var (
9971001
"deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary",
9981002
}
9991003
)
1004+
1005+
var godirectives = map[string]struct{}{
1006+
// https://pkg.go.dev/cmd/compile
1007+
"noescape": {},
1008+
"uintptrescapes": {},
1009+
"noinline": {},
1010+
"norace": {},
1011+
"nosplit": {},
1012+
"linkname": {},
1013+
1014+
// https://pkg.go.dev/go/build
1015+
"build": {},
1016+
"binary-only-package": {},
1017+
"embed": {},
1018+
}
1019+
1020+
// Tokenize godirective at the start of the comment c, if any, and the surrounding comment.
1021+
// If there is any failure, emits the entire comment as a tokComment token.
1022+
// Directives are highlighted as-is, even if used incorrectly. Typically there are
1023+
// dedicated analyzers that will warn about misuse.
1024+
func (e *encoded) godirective(c *ast.Comment) {
1025+
// First check if '//go:directive args...' is a valid directive.
1026+
directive, args, _ := strings.Cut(c.Text, " ")
1027+
kind, _ := stringsCutPrefix(directive, "//go:")
1028+
if _, ok := godirectives[kind]; !ok {
1029+
// Unknown go: directive.
1030+
e.token(c.Pos(), len(c.Text), tokComment, nil)
1031+
return
1032+
}
1033+
1034+
// Make the 'go:directive' part stand out, the rest is comments.
1035+
e.token(c.Pos(), len("//"), tokComment, nil)
1036+
1037+
directiveStart := c.Pos() + token.Pos(len("//"))
1038+
e.token(directiveStart, len(directive[len("//"):]), tokNamespace, nil)
1039+
1040+
if len(args) > 0 {
1041+
tailStart := c.Pos() + token.Pos(len(directive)+len(" "))
1042+
e.token(tailStart, len(args), tokComment, nil)
1043+
}
1044+
}
1045+
1046+
// Go 1.20 strings.CutPrefix.
1047+
func stringsCutPrefix(s, prefix string) (after string, found bool) {
1048+
if !strings.HasPrefix(s, prefix) {
1049+
return s, false
1050+
}
1051+
return s[len(prefix):], true
1052+
}

gopls/internal/regtest/misc/semantictokens_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,60 @@ func New[K int, V any]() Smap[K, V] {
145145
}
146146
})
147147
}
148+
149+
func TestSemanticGoDirectives(t *testing.T) {
150+
src := `
151+
-- go.mod --
152+
module example.com
153+
154+
go 1.19
155+
-- main.go --
156+
package foo
157+
158+
//go:linkname now time.Now
159+
func now()
160+
161+
//go:noinline
162+
func foo() {}
163+
164+
// Mentioning go:noinline should not tokenize.
165+
166+
//go:notadirective
167+
func bar() {}
168+
`
169+
want := []fake.SemanticToken{
170+
{Token: "package", TokenType: "keyword"},
171+
{Token: "foo", TokenType: "namespace"},
172+
173+
{Token: "//", TokenType: "comment"},
174+
{Token: "go:linkname", TokenType: "namespace"},
175+
{Token: "now time.Now", TokenType: "comment"},
176+
{Token: "func", TokenType: "keyword"},
177+
{Token: "now", TokenType: "function", Mod: "definition"},
178+
179+
{Token: "//", TokenType: "comment"},
180+
{Token: "go:noinline", TokenType: "namespace"},
181+
{Token: "func", TokenType: "keyword"},
182+
{Token: "foo", TokenType: "function", Mod: "definition"},
183+
184+
{Token: "// Mentioning go:noinline should not tokenize.", TokenType: "comment"},
185+
186+
{Token: "//go:notadirective", TokenType: "comment"},
187+
{Token: "func", TokenType: "keyword"},
188+
{Token: "bar", TokenType: "function", Mod: "definition"},
189+
}
190+
191+
WithOptions(
192+
Modes(Default),
193+
Settings{"semanticTokens": true},
194+
).Run(t, src, func(t *testing.T, env *Env) {
195+
env.OpenFile("main.go")
196+
seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go")
197+
if err != nil {
198+
t.Fatal(err)
199+
}
200+
if x := cmp.Diff(want, seen); x != "" {
201+
t.Errorf("Semantic tokens do not match (-want +got):\n%s", x)
202+
}
203+
})
204+
}

0 commit comments

Comments
 (0)