Skip to content

Commit 24677c6

Browse files
committed
zz
1 parent 2f9c834 commit 24677c6

File tree

2 files changed

+324
-2
lines changed

2 files changed

+324
-2
lines changed

gopls/internal/golang/highlight.go

+284-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import (
1010
"go/ast"
1111
"go/token"
1212
"go/types"
13+
"io"
14+
"strings"
1315

1416
"golang.org/x/tools/go/ast/astutil"
1517
"golang.org/x/tools/gopls/internal/cache"
1618
"golang.org/x/tools/gopls/internal/file"
1719
"golang.org/x/tools/gopls/internal/protocol"
20+
gastutil "golang.org/x/tools/gopls/internal/util/astutil"
1821
"golang.org/x/tools/internal/event"
1922
)
2023

@@ -49,7 +52,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
4952
}
5053
}
5154
}
52-
result, err := highlightPath(path, pgf.File, pkg.TypesInfo())
55+
result, err := highlightPath(path, pgf.File, pkg.TypesInfo(), pos)
5356
if err != nil {
5457
return nil, err
5558
}
@@ -69,8 +72,20 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
6972

7073
// highlightPath returns ranges to highlight for the given enclosing path,
7174
// 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) {
75+
func highlightPath(path []ast.Node, file *ast.File, info *types.Info, pos token.Pos) (map[posRange]protocol.DocumentHighlightKind, error) {
7376
result := make(map[posRange]protocol.DocumentHighlightKind)
77+
// Inside a printf-style call?
78+
for _, node := range path {
79+
if call, ok := node.(*ast.CallExpr); ok {
80+
for _, args := range call.Args {
81+
// Only try when pos is in right side of the format String.
82+
if basicList, ok := args.(*ast.BasicLit); ok && basicList.Pos() < pos &&
83+
basicList.Kind == token.STRING && strings.Contains(basicList.Value, "%") {
84+
highlightPrintf(basicList, call, pos, result)
85+
}
86+
}
87+
}
88+
}
7489
switch node := path[0].(type) {
7590
case *ast.BasicLit:
7691
// Import path string literal?
@@ -131,6 +146,273 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa
131146
return result, nil
132147
}
133148

149+
// highlightPrintf identifies and highlights the relationships between placeholders
150+
// in a format string and their corresponding variadic arguments in a printf-style
151+
// function call.
152+
//
153+
// For example:
154+
//
155+
// fmt.Printf("Hello %s, you scored %d", name, score)
156+
//
157+
// If the cursor is on %s or name, highlightPrintf will highlight %s as a write operation,
158+
// and name as a read operation.
159+
func highlightPrintf(directive *ast.BasicLit, call *ast.CallExpr, pos token.Pos, result map[posRange]protocol.DocumentHighlightKind) {
160+
// Two '%'s are interpreted as one '%'(escaped), let's replace them with spaces.
161+
format := strings.Replace(directive.Value, "%%", " ", -1)
162+
// Give up when encounter '% %'.
163+
// For example:
164+
//
165+
// fmt.Printf("hello % %s, %-2.3d\n", "world", 123)
166+
//
167+
// The implementation of fmt.doPrintf will ignore first two '%'s,
168+
// causing arguments count bigger than placeholders count (2 > 1), producing
169+
// "%!(EXTRA" error string in formatFunc and incorrect highlight range.
170+
for i := 0; i < len(format); i++ {
171+
if format[i] == '%' {
172+
for j := i + 1; j < len(format); j++ {
173+
c := format[j]
174+
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
175+
i = j
176+
break
177+
}
178+
if c == '%' {
179+
return
180+
}
181+
}
182+
}
183+
}
184+
185+
// Make sure variadic arguments passed to parsef matches correct count of '%'.
186+
expectedVariadicArgs := make([]ast.Expr, strings.Count(format, "%"))
187+
firstVariadic := -1
188+
for i, arg := range call.Args {
189+
if directive == arg {
190+
firstVariadic = i + 1
191+
argsLen := len(call.Args) - i - 1
192+
if argsLen > len(expectedVariadicArgs) {
193+
// Translate from Printf(a0,"%d %d",5, 6, 7) to [5, 6]
194+
copy(expectedVariadicArgs, call.Args[firstVariadic:firstVariadic+len(expectedVariadicArgs)])
195+
} else {
196+
// Translate from Printf(a0,"%d %d %s",5, 6) to [5, 6, nil]
197+
copy(expectedVariadicArgs[:argsLen], call.Args[firstVariadic:])
198+
}
199+
break
200+
}
201+
}
202+
var percent formatPercent
203+
// Get a position-ordered slice describing each directive item.
204+
formatItems := parsef(format, directive.Pos(), expectedVariadicArgs...)
205+
// Cursor in argument.
206+
if pos > directive.End() {
207+
// Which variadic argument cursor sits inside.
208+
for i := firstVariadic; i < len(call.Args); i++ {
209+
if gastutil.NodeContains(call.Args[i], pos) {
210+
// Offset relative to parsedDirectives.
211+
// (Printf(a0,"%d %d %s",5, 6), firstVariadic=2,i=3)
212+
// ^ cursor here
213+
// -> ([5, 6, nil], firstVariadic=1)
214+
// ^
215+
firstVariadic = i - firstVariadic
216+
break
217+
}
218+
}
219+
index := -1
220+
for _, item := range formatItems {
221+
switch item := item.(type) {
222+
case formatPercent:
223+
percent = item
224+
index++
225+
case formatVerb:
226+
if token.Pos(percent).IsValid() {
227+
if index == firstVariadic {
228+
// Placeholders behave like writting values from arguments to themselves,
229+
// so highlight them with Write semantic.
230+
highlightRange(result, token.Pos(percent), item.rang.end, protocol.Write)
231+
highlightRange(result, item.operand.Pos(), item.operand.End(), protocol.Read)
232+
return
233+
}
234+
percent = formatPercent(token.NoPos)
235+
}
236+
}
237+
}
238+
} else {
239+
// Cursor in format string.
240+
for _, item := range formatItems {
241+
switch item := item.(type) {
242+
case formatPercent:
243+
percent = item
244+
case formatVerb:
245+
if token.Pos(percent).IsValid() {
246+
if token.Pos(percent) <= pos && pos <= item.rang.end {
247+
highlightRange(result, token.Pos(percent), item.rang.end, protocol.Write)
248+
if item.operand != nil {
249+
highlightRange(result, item.operand.Pos(), item.operand.End(), protocol.Read)
250+
}
251+
return
252+
}
253+
percent = formatPercent(token.NoPos)
254+
}
255+
}
256+
}
257+
}
258+
}
259+
260+
// Below are formatting directives definitions.
261+
type formatPercent token.Pos
262+
type formatLiteral struct {
263+
literal string
264+
rang posRange
265+
}
266+
type formatFlags struct {
267+
flag string
268+
rang posRange
269+
}
270+
type formatWidth struct {
271+
width int
272+
rang posRange
273+
}
274+
type formatPrec struct {
275+
prec int
276+
rang posRange
277+
}
278+
type formatVerb struct {
279+
verb rune
280+
rang posRange
281+
operand ast.Expr // verb's corresponding operand, may be nil
282+
}
283+
284+
type formatItem interface {
285+
formatItem()
286+
}
287+
288+
func (formatPercent) formatItem() {}
289+
func (formatLiteral) formatItem() {}
290+
func (formatVerb) formatItem() {}
291+
func (formatWidth) formatItem() {}
292+
func (formatFlags) formatItem() {}
293+
func (formatPrec) formatItem() {}
294+
295+
type formatFunc func(fmt.State, rune)
296+
297+
var _ fmt.Formatter = formatFunc(nil)
298+
299+
func (f formatFunc) Format(st fmt.State, verb rune) { f(st, verb) }
300+
301+
// parsef parses a printf-style format string into its constituent components together with
302+
// their position in the source code, including [formatLiteral], formatting directives
303+
// [formatFlags], [formatPrecision], [formatWidth], [formatPrecision], [formatVerb], and its operand.
304+
//
305+
// If format contains explicit argument indexes, eg. fmt.Sprintf("%[2]d %[1]d\n", 11, 22),
306+
// or directives count does not match argument count, the returned range will not be correct.
307+
func parsef(format string, pos token.Pos, args ...ast.Expr) []formatItem {
308+
const sep = "!!!GOPLS_SEP!!!"
309+
// A conversion represents a single % operation and its operand.
310+
type conversion struct {
311+
verb rune
312+
width int // or -1
313+
prec int // or -1
314+
flag string // some of "-+# 0"
315+
operand ast.Expr
316+
}
317+
var convs []conversion
318+
wrappers := make([]any, len(args))
319+
for i, operand := range args {
320+
wrappers[i] = formatFunc(func(st fmt.State, verb rune) {
321+
io.WriteString(st, sep)
322+
width, ok := st.Width()
323+
if !ok {
324+
width = -1
325+
}
326+
prec, ok := st.Precision()
327+
if !ok {
328+
prec = -1
329+
}
330+
flag := ""
331+
for _, b := range "-+# 0" {
332+
if st.Flag(int(b)) {
333+
flag += string(b)
334+
}
335+
}
336+
convs = append(convs, conversion{
337+
verb: verb,
338+
width: width,
339+
prec: prec,
340+
flag: flag,
341+
operand: operand,
342+
})
343+
})
344+
}
345+
346+
// Interleave the literals and the conversions.
347+
var formatItems []formatItem
348+
for i, word := range strings.Split(fmt.Sprintf(format, wrappers...), sep) {
349+
if word != "" {
350+
formatItems = append(formatItems, formatLiteral{
351+
literal: word,
352+
rang: posRange{
353+
start: pos,
354+
end: pos + token.Pos(len(word)),
355+
},
356+
})
357+
pos = pos + token.Pos(len(word))
358+
}
359+
if i < len(convs) {
360+
conv := convs[i]
361+
// Collect %.
362+
formatItems = append(formatItems, formatPercent(pos))
363+
pos += 1
364+
// Collect flags.
365+
if flag := conv.flag; flag != "" {
366+
length := token.Pos(len(conv.flag))
367+
formatItems = append(formatItems, formatFlags{
368+
flag: flag,
369+
rang: posRange{
370+
start: pos,
371+
end: pos + length,
372+
},
373+
})
374+
pos += length
375+
}
376+
// Collect width.
377+
if width := conv.width; conv.width != -1 {
378+
length := token.Pos(len(fmt.Sprintf("%d", conv.width)))
379+
formatItems = append(formatItems, formatWidth{
380+
width: width,
381+
rang: posRange{
382+
start: pos,
383+
end: pos + length,
384+
},
385+
})
386+
pos += length
387+
}
388+
// Collect precision, which starts with a dot.
389+
if prec := conv.prec; conv.prec != -1 {
390+
length := token.Pos(len(fmt.Sprintf("%d", conv.prec))) + 1
391+
formatItems = append(formatItems, formatPrec{
392+
prec: prec,
393+
rang: posRange{
394+
start: pos,
395+
end: pos + length,
396+
},
397+
})
398+
pos += length
399+
}
400+
// Collect verb, which must be present.
401+
length := token.Pos(len(string(conv.verb)))
402+
formatItems = append(formatItems, formatVerb{
403+
verb: conv.verb,
404+
rang: posRange{
405+
start: pos,
406+
end: pos + length,
407+
},
408+
operand: conv.operand,
409+
})
410+
pos += length
411+
}
412+
}
413+
return formatItems
414+
}
415+
134416
type posRange struct {
135417
start, end token.Pos
136418
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
This test checks functionality of the printf-like directives and operands highlight.
2+
3+
-- flags --
4+
-ignore_extra_diags
5+
6+
-- highlights.go --
7+
package highlightprintf
8+
9+
import (
10+
"fmt"
11+
)
12+
13+
func BasicPrintfHighlights() {
14+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normals, "%s", write),hiloc(normalarg0, "\"Alice\"", read),highlightall(normals, normalarg0)
15+
fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normald, "%d", write),hiloc(normalargs1, "5", read),highlightall(normald, normalargs1)
16+
}
17+
18+
func ComplexPrintfHighlights() {
19+
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)
20+
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)
21+
}
22+
23+
func MissingDirectives() {
24+
fmt.Printf("Hello %s, you have 5 new messages!", "Alice", 5) //@hiloc(missings, "%s", write),hiloc(missingargs0, "\"Alice\"", read),highlightall(missings, missingargs0)
25+
}
26+
27+
func TooManyDirectives() {
28+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanys, "%s", write),hiloc(toomanyargs0, "\"Alice\"", read),highlightall(toomanys, toomanyargs0)
29+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanyd, "%d", write),hiloc(toomanyargs1, "5", read),highlightall(toomanyd, toomanyargs1)
30+
}
31+
32+
func SpecialChars() {
33+
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)
34+
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)
35+
}
36+
37+
func Escaped() {
38+
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)
39+
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)
40+
}

0 commit comments

Comments
 (0)