Skip to content

Commit 7d99ad7

Browse files
xzbdmwgopherbot
authored andcommitted
gopls/internal/highlight: DocumentHighlight for format strings
This CL introduces functionality for highlighting printf-style directives and their associated variadic arguments. Also fix some comments/names in CL 623156 Updates golang/go#70050 Change-Id: I1c4e6678317aa8cf522f41765f5a6600793f3746 GitHub-Last-Rev: d15a3b3 GitHub-Pull-Request: #555 Reviewed-on: https://go-review.googlesource.com/c/tools/+/642095 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Robert Findley <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 4403100 commit 7d99ad7

File tree

6 files changed

+253
-8
lines changed

6 files changed

+253
-8
lines changed

gopls/doc/release/v0.18.0.md

+12
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,15 @@ The Definition query now supports additional locations:
9797

9898
When invoked on a return statement, hover reports the types of
9999
the function's result variables.
100+
101+
## Improvements to "DocumentHighlight"
102+
103+
When your cursor is inside a printf-like function, gopls now highlights the relationship between
104+
formatting verbs and arguments as visual cues to differentiate how operands are used in the format string.
105+
106+
```go
107+
fmt.Printf("Hello %s, you scored %d", name, score)
108+
```
109+
110+
If the cursor is either on `%s` or `name`, gopls will highlight `%s` as a write operation,
111+
and `name` as a read operation.

gopls/internal/golang/codeaction.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,9 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
335335
req.addApplyFixAction(msg, fixMissingCalledFunction, req.loc)
336336
}
337337

338-
// "undeclared name: x" or "undefined: x" compiler error.
339-
// Offer a "Create variable/function x" code action.
340-
// See [fixUndeclared] for command implementation.
338+
// "undeclared name: X" or "undefined: X" compiler error.
339+
// Offer a "Create variable/function X" code action.
340+
// See [createUndeclared] for command implementation.
341341
case strings.HasPrefix(msg, "undeclared name: "),
342342
strings.HasPrefix(msg, "undefined: "):
343343
path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)

gopls/internal/golang/fix.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
112112
fixInvertIfCondition: singleFile(invertIfCondition),
113113
fixSplitLines: singleFile(splitLines),
114114
fixJoinLines: singleFile(joinLines),
115-
fixCreateUndeclared: singleFile(CreateUndeclared),
115+
fixCreateUndeclared: singleFile(createUndeclared),
116116
fixMissingInterfaceMethods: stubMissingInterfaceMethodsFixer,
117117
fixMissingCalledFunction: stubMissingCalledFunctionFixer,
118118
}

gopls/internal/golang/highlight.go

+180-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import (
1010
"go/ast"
1111
"go/token"
1212
"go/types"
13+
"strconv"
14+
"strings"
15+
"unicode/utf8"
1316

1417
"golang.org/x/tools/go/ast/astutil"
1518
"golang.org/x/tools/gopls/internal/cache"
1619
"golang.org/x/tools/gopls/internal/file"
1720
"golang.org/x/tools/gopls/internal/protocol"
1821
"golang.org/x/tools/internal/event"
22+
"golang.org/x/tools/internal/fmtstr"
1923
)
2024

2125
func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.DocumentHighlight, error) {
@@ -49,7 +53,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
4953
}
5054
}
5155
}
52-
result, err := highlightPath(path, pgf.File, pkg.TypesInfo())
56+
result, err := highlightPath(pkg.TypesInfo(), path, pos)
5357
if err != nil {
5458
return nil, err
5559
}
@@ -69,8 +73,22 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
6973

7074
// highlightPath returns ranges to highlight for the given enclosing path,
7175
// which should be the result of astutil.PathEnclosingInterval.
72-
func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]protocol.DocumentHighlightKind, error) {
76+
func highlightPath(info *types.Info, path []ast.Node, pos token.Pos) (map[posRange]protocol.DocumentHighlightKind, error) {
7377
result := make(map[posRange]protocol.DocumentHighlightKind)
78+
79+
// Inside a call to a printf-like function (as identified
80+
// by a simple heuristic).
81+
// Treat each corresponding ("%v", arg) pair as a highlight class.
82+
for _, node := range path {
83+
if call, ok := node.(*ast.CallExpr); ok {
84+
lit, idx := formatStringAndIndex(info, call)
85+
if idx != -1 {
86+
highlightPrintf(call, idx, pos, lit, result)
87+
}
88+
}
89+
}
90+
91+
file := path[len(path)-1].(*ast.File)
7492
switch node := path[0].(type) {
7593
case *ast.BasicLit:
7694
// Import path string literal?
@@ -131,6 +149,166 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa
131149
return result, nil
132150
}
133151

152+
// formatStringAndIndex returns the BasicLit and index of the BasicLit (the last
153+
// non-variadic parameter) within the given printf-like call
154+
// expression, returns -1 as index if unknown.
155+
func formatStringAndIndex(info *types.Info, call *ast.CallExpr) (*ast.BasicLit, int) {
156+
typ := info.Types[call.Fun].Type
157+
if typ == nil {
158+
return nil, -1 // missing type
159+
}
160+
sig, ok := typ.(*types.Signature)
161+
if !ok {
162+
return nil, -1 // ill-typed
163+
}
164+
if !sig.Variadic() {
165+
// Skip checking non-variadic functions.
166+
return nil, -1
167+
}
168+
idx := sig.Params().Len() - 2
169+
if idx < 0 {
170+
// Skip checking variadic functions without
171+
// fixed arguments.
172+
return nil, -1
173+
}
174+
// We only care about literal format strings, so fmt.Sprint("a"+"b%s", "bar") won't be highlighted.
175+
if lit, ok := call.Args[idx].(*ast.BasicLit); ok && lit.Kind == token.STRING {
176+
return lit, idx
177+
}
178+
return nil, -1
179+
}
180+
181+
// highlightPrintf highlights operations in a format string and their corresponding
182+
// variadic arguments in a (possible) printf-style function call.
183+
// For example:
184+
//
185+
// fmt.Printf("Hello %s, you scored %d", name, score)
186+
//
187+
// If the cursor is on %s or name, it will highlight %s as a write operation,
188+
// and name as a read operation.
189+
func highlightPrintf(call *ast.CallExpr, idx int, cursorPos token.Pos, lit *ast.BasicLit, result map[posRange]protocol.DocumentHighlightKind) {
190+
format, err := strconv.Unquote(lit.Value)
191+
if err != nil {
192+
return
193+
}
194+
if !strings.Contains(format, "%") {
195+
return
196+
}
197+
operations, err := fmtstr.Parse(format, idx)
198+
if err != nil {
199+
return
200+
}
201+
202+
// fmt.Printf("%[1]d %[1].2d", 3)
203+
//
204+
// When cursor is in `%[1]d`, we record `3` being successfully highlighted.
205+
// And because we will also record `%[1].2d`'s corresponding arguments index is `3`
206+
// in `visited`, even though it will not highlight any item in the first pass,
207+
// in the second pass we can correctly highlight it. So the three are the same class.
208+
succeededArg := 0
209+
visited := make(map[posRange]int, 0)
210+
211+
// highlightPair highlights the operation and its potential argument pair if the cursor is within either range.
212+
highlightPair := func(rang fmtstr.Range, argIndex int) {
213+
rangeStart, err := posInStringLiteral(lit, rang.Start)
214+
if err != nil {
215+
return
216+
}
217+
rangeEnd, err := posInStringLiteral(lit, rang.End)
218+
if err != nil {
219+
return
220+
}
221+
visited[posRange{rangeStart, rangeEnd}] = argIndex
222+
223+
var arg ast.Expr
224+
if argIndex < len(call.Args) {
225+
arg = call.Args[argIndex]
226+
}
227+
228+
// cursorPos can't equal to end position, otherwise the two
229+
// neighborhood such as (%[2]*d) are both highlighted if cursor in "*" (ending of [2]*).
230+
if rangeStart <= cursorPos && cursorPos < rangeEnd ||
231+
arg != nil && arg.Pos() <= cursorPos && cursorPos < arg.End() {
232+
highlightRange(result, rangeStart, rangeEnd, protocol.Write)
233+
if arg != nil {
234+
succeededArg = argIndex
235+
highlightRange(result, arg.Pos(), arg.End(), protocol.Read)
236+
}
237+
}
238+
}
239+
240+
for _, op := range operations {
241+
// If width or prec has any *, we can not highlight the full range from % to verb,
242+
// because it will overlap with the sub-range of *, for example:
243+
//
244+
// fmt.Printf("%*[3]d", 4, 5, 6)
245+
// ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will
246+
// highlight for 4.
247+
hasAsterisk := false
248+
249+
// Try highlight Width if there is a *.
250+
if op.Width.Dynamic != -1 {
251+
hasAsterisk = true
252+
highlightPair(op.Width.Range, op.Width.Dynamic)
253+
}
254+
255+
// Try highlight Precision if there is a *.
256+
if op.Prec.Dynamic != -1 {
257+
hasAsterisk = true
258+
highlightPair(op.Prec.Range, op.Prec.Dynamic)
259+
}
260+
261+
// Try highlight Verb.
262+
if op.Verb.Verb != '%' {
263+
// If any * is found inside operation, narrow the highlight range.
264+
if hasAsterisk {
265+
highlightPair(op.Verb.Range, op.Verb.ArgIndex)
266+
} else {
267+
highlightPair(op.Range, op.Verb.ArgIndex)
268+
}
269+
}
270+
}
271+
272+
// Second pass, try to highlight those missed operations.
273+
for rang, argIndex := range visited {
274+
if succeededArg == argIndex {
275+
highlightRange(result, rang.start, rang.end, protocol.Write)
276+
}
277+
}
278+
}
279+
280+
// posInStringLiteral returns the position within a string literal
281+
// corresponding to the specified byte offset within the logical
282+
// string that it denotes.
283+
func posInStringLiteral(lit *ast.BasicLit, offset int) (token.Pos, error) {
284+
raw := lit.Value
285+
286+
value, err := strconv.Unquote(raw)
287+
if err != nil {
288+
return 0, err
289+
}
290+
if !(0 <= offset && offset <= len(value)) {
291+
return 0, fmt.Errorf("invalid offset")
292+
}
293+
294+
// remove quotes
295+
quote := raw[0] // '"' or '`'
296+
raw = raw[1 : len(raw)-1]
297+
298+
var (
299+
i = 0 // byte index within logical value
300+
pos = lit.ValuePos + 1 // position within literal
301+
)
302+
for raw != "" && i < offset {
303+
r, _, rest, _ := strconv.UnquoteChar(raw, quote) // can't fail
304+
sz := len(raw) - len(rest) // length of literal char in raw bytes
305+
pos += token.Pos(sz)
306+
raw = raw[sz:]
307+
i += utf8.RuneLen(r)
308+
}
309+
return pos, nil
310+
}
311+
134312
type posRange struct {
135313
start, end token.Pos
136314
}

gopls/internal/golang/undeclared.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ func undeclaredFixTitle(path []ast.Node, errMsg string) string {
6868
return fmt.Sprintf("Create %s %s", noun, name)
6969
}
7070

71-
// CreateUndeclared generates a suggested declaration for an undeclared variable or function.
72-
func CreateUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
71+
// createUndeclared generates a suggested declaration for an undeclared variable or function.
72+
func createUndeclared(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
7373
pos := start // don't use the end
7474
path, _ := astutil.PathEnclosingInterval(file, pos, pos)
7575
if len(path) < 2 {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
This test checks functionality of the printf-like directives and operands highlight.
3+
-- flags --
4+
-ignore_extra_diags
5+
-- highlights.go --
6+
package highlightprintf
7+
import (
8+
"fmt"
9+
)
10+
11+
func BasicPrintfHighlights() {
12+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normals, "%s", write),hiloc(normalarg0, "\"Alice\"", read),highlightall(normals, normalarg0)
13+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normald, "%d", write),hiloc(normalargs1, "5", read),highlightall(normald, normalargs1)
14+
}
15+
16+
func ComplexPrintfHighlights() {
17+
fmt.Printf("Hello %#3.4s, you have %-2.3d new messages!", "Alice", 5) //@hiloc(complexs, "%#3.4s", write),hiloc(complexarg0, "\"Alice\"", read),highlightall(complexs, complexarg0)
18+
fmt.Printf("Hello %#3.4s, you have %-2.3d new messages!", "Alice", 5) //@hiloc(complexd, "%-2.3d", write),hiloc(complexarg1, "5", read),highlightall(complexd, complexarg1)
19+
}
20+
21+
func MissingDirectives() {
22+
fmt.Printf("Hello %s, you have 5 new messages!", "Alice", 5) //@hiloc(missings, "%s", write),hiloc(missingargs0, "\"Alice\"", read),highlightall(missings, missingargs0)
23+
}
24+
25+
func TooManyDirectives() {
26+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanys, "%s", write),hiloc(toomanyargs0, "\"Alice\"", read),highlightall(toomanys, toomanyargs0)
27+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanyd, "%d", write),hiloc(toomanyargs1, "5", read),highlightall(toomanyd, toomanyargs1)
28+
}
29+
30+
func VerbIsPercentage() {
31+
fmt.Printf("%4.2% %d", 6) //@hiloc(z1, "%d", write),hiloc(z2, "6", read),highlightall(z1, z2)
32+
}
33+
34+
func SpecialChars() {
35+
fmt.Printf("Hello \n %s, you \t \n have %d new messages!", "Alice", 5) //@hiloc(specials, "%s", write),hiloc(specialargs0, "\"Alice\"", read),highlightall(specials, specialargs0)
36+
fmt.Printf("Hello \n %s, you \t \n have %d new messages!", "Alice", 5) //@hiloc(speciald, "%d", write),hiloc(specialargs1, "5", read),highlightall(speciald, specialargs1)
37+
}
38+
39+
func Escaped() {
40+
fmt.Printf("Hello %% \n %s, you \t%% \n have %d new m%%essages!", "Alice", 5) //@hiloc(escapeds, "%s", write),hiloc(escapedargs0, "\"Alice\"", read),highlightall(escapeds, escapedargs0)
41+
fmt.Printf("Hello %% \n %s, you \t%% \n have %d new m%%essages!", "Alice", 5) //@hiloc(escapedd, "%s", write),hiloc(escapedargs1, "\"Alice\"", read),highlightall(escapedd, escapedargs1)
42+
fmt.Printf("%d \nss \x25[2]d", 234, 123) //@hiloc(zz1, "%d", write),hiloc(zz2, "234", read),highlightall(zz1,zz2)
43+
fmt.Printf("%d \nss \x25[2]d", 234, 123) //@hiloc(zz3, "\\x25[2]d", write),hiloc(zz4, "123", read),highlightall(zz3,zz4)
44+
}
45+
46+
func Indexed() {
47+
fmt.Printf("%[1]d", 3) //@hiloc(i1, "%[1]d", write),hiloc(i2, "3", read),highlightall(i1, i2)
48+
fmt.Printf("%[1]*d", 3, 6) //@hiloc(i3, "[1]*", write),hiloc(i4, "3", read),hiloc(i5, "d", write),hiloc(i6, "6", read),highlightall(i3, i4),highlightall(i5, i6)
49+
fmt.Printf("%[2]*[1]d", 3, 4) //@hiloc(i7, "[2]*", write),hiloc(i8, "4", read),hiloc(i9, "[1]d", write),hiloc(i10, "3", read),highlightall(i7, i8),highlightall(i9, i10)
50+
fmt.Printf("%[2]*.[1]*[3]d", 4, 5, 6) //@hiloc(i11, "[2]*", write),hiloc(i12, "5", read),hiloc(i13, ".[1]*", write),hiloc(i14, "4", read),hiloc(i15, "[3]d", write),hiloc(i16, "6", read),highlightall(i11, i12),highlightall(i13, i14),highlightall(i15, i16)
51+
}
52+
53+
func MultipleIndexed() {
54+
fmt.Printf("%[1]d %[1].2d", 3) //@hiloc(m1, "%[1]d", write),hiloc(m2, "3", read),hiloc(m3, "%[1].2d", write),highlightall(m1, m2, m3)
55+
}

0 commit comments

Comments
 (0)