Skip to content

Commit d107a5e

Browse files
committed
zz
1 parent 2f9c834 commit d107a5e

File tree

2 files changed

+311
-2
lines changed

2 files changed

+311
-2
lines changed

gopls/internal/golang/highlight.go

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

1415
"golang.org/x/tools/go/ast/astutil"
1516
"golang.org/x/tools/gopls/internal/cache"
1617
"golang.org/x/tools/gopls/internal/file"
1718
"golang.org/x/tools/gopls/internal/protocol"
19+
gastutil "golang.org/x/tools/gopls/internal/util/astutil"
1820
"golang.org/x/tools/internal/event"
1921
)
2022

@@ -49,7 +51,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
4951
}
5052
}
5153
}
52-
result, err := highlightPath(path, pgf.File, pkg.TypesInfo())
54+
result, err := highlightPath(path, pgf.File, pkg.TypesInfo(), pos)
5355
if err != nil {
5456
return nil, err
5557
}
@@ -69,8 +71,20 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po
6971

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

148+
// highlightPrintf identifies and highlights the relationships between placeholders
149+
// in a format string and their corresponding variadic arguments in a printf-style
150+
// function call.
151+
//
152+
// For example:
153+
//
154+
// fmt.Printf("Hello %s, you scored %d", name, score)
155+
//
156+
// If the cursor is on %s or name, highlightPrintf will highlight %s as a write operation,
157+
// and name as a read operation.
158+
func highlightPrintf(directive *ast.BasicLit, call *ast.CallExpr, pos token.Pos, result map[posRange]protocol.DocumentHighlightKind) {
159+
// Two '%'s are interpreted as one '%'(escaped), let's replace them with spaces.
160+
format := strings.Replace(directive.Value, "%%", " ", -1)
161+
if strings.Contains(directive.Value, "%[") ||
162+
strings.Contains(directive.Value, "%p") ||
163+
strings.Contains(directive.Value, "%T") {
164+
// parsef can not handle these cases.
165+
return
166+
}
167+
expectedVariadicArgs := make([]ast.Expr, strings.Count(format, "%"))
168+
firstVariadic := -1
169+
for i, arg := range call.Args {
170+
if directive == arg {
171+
firstVariadic = i + 1
172+
argsLen := len(call.Args) - i - 1
173+
if argsLen > len(expectedVariadicArgs) {
174+
// Translate from Printf(a0,"%d %d",5, 6, 7) to [5, 6]
175+
copy(expectedVariadicArgs, call.Args[firstVariadic:firstVariadic+len(expectedVariadicArgs)])
176+
} else {
177+
// Translate from Printf(a0,"%d %d %s",5, 6) to [5, 6, nil]
178+
copy(expectedVariadicArgs[:argsLen], call.Args[firstVariadic:])
179+
}
180+
break
181+
}
182+
}
183+
formatItems, err := parsef(format, directive.Pos(), expectedVariadicArgs...)
184+
if err != nil {
185+
return
186+
}
187+
var percent formatPercent
188+
// Cursor in argument.
189+
if pos > directive.End() {
190+
var curVariadic int
191+
// Which variadic argument cursor sits inside.
192+
for i := firstVariadic; i < len(call.Args); i++ {
193+
if gastutil.NodeContains(call.Args[i], pos) {
194+
// Offset relative to formatItems.
195+
curVariadic = i - firstVariadic
196+
break
197+
}
198+
}
199+
index := -1
200+
for _, item := range formatItems {
201+
switch item := item.(type) {
202+
case formatPercent:
203+
percent = item
204+
index++
205+
case formatVerb:
206+
if token.Pos(percent).IsValid() {
207+
if index == curVariadic {
208+
// Placeholders behave like writting values from arguments to themselves,
209+
// so highlight them with Write semantic.
210+
highlightRange(result, token.Pos(percent), item.rang.end, protocol.Write)
211+
highlightRange(result, item.operand.Pos(), item.operand.End(), protocol.Read)
212+
return
213+
}
214+
percent = formatPercent(token.NoPos)
215+
}
216+
}
217+
}
218+
} else {
219+
// Cursor in format string.
220+
for _, item := range formatItems {
221+
switch item := item.(type) {
222+
case formatPercent:
223+
percent = item
224+
case formatVerb:
225+
if token.Pos(percent).IsValid() {
226+
if token.Pos(percent) <= pos && pos <= item.rang.end {
227+
highlightRange(result, token.Pos(percent), item.rang.end, protocol.Write)
228+
if item.operand != nil {
229+
highlightRange(result, item.operand.Pos(), item.operand.End(), protocol.Read)
230+
}
231+
return
232+
}
233+
percent = formatPercent(token.NoPos)
234+
}
235+
}
236+
}
237+
}
238+
}
239+
240+
// Below are formatting directives definitions.
241+
type formatPercent token.Pos
242+
type formatLiteral struct {
243+
literal string
244+
rang posRange
245+
}
246+
type formatFlags struct {
247+
flag string
248+
rang posRange
249+
}
250+
type formatWidth struct {
251+
width int
252+
rang posRange
253+
}
254+
type formatPrec struct {
255+
prec int
256+
rang posRange
257+
}
258+
type formatVerb struct {
259+
verb rune
260+
rang posRange
261+
operand ast.Expr // verb's corresponding operand, may be nil
262+
}
263+
264+
type formatItem interface {
265+
formatItem()
266+
}
267+
268+
func (formatPercent) formatItem() {}
269+
func (formatLiteral) formatItem() {}
270+
func (formatVerb) formatItem() {}
271+
func (formatWidth) formatItem() {}
272+
func (formatFlags) formatItem() {}
273+
func (formatPrec) formatItem() {}
274+
275+
type formatFunc func(fmt.State, rune)
276+
277+
var _ fmt.Formatter = formatFunc(nil)
278+
279+
func (f formatFunc) Format(st fmt.State, verb rune) { f(st, verb) }
280+
281+
// parsef parses a printf-style format string into its constituent components together with
282+
// their position in the source code, including [formatLiteral], formatting directives
283+
// [formatFlags], [formatPrecision], [formatWidth], [formatPrecision], [formatVerb], and its operand.
284+
//
285+
// If format contains explicit argument indexes, eg. fmt.Sprintf("%[2]d %[1]d\n", 11, 22),
286+
// the returned range will not be correct.
287+
// If an invalid argument is given for a verb, such as providing a string to %d, the returned error will
288+
// contain a description of the problem.
289+
func parsef(format string, pos token.Pos, args ...ast.Expr) ([]formatItem, error) {
290+
const sep = "__GOPLS_SEP__"
291+
// A conversion represents a single % operation and its operand.
292+
type conversion struct {
293+
verb rune
294+
width int // or -1
295+
prec int // or -1
296+
flag string // some of "-+# 0"
297+
operand ast.Expr
298+
}
299+
var convs []conversion
300+
wrappers := make([]any, len(args))
301+
for i, operand := range args {
302+
wrappers[i] = formatFunc(func(st fmt.State, verb rune) {
303+
st.Write([]byte(sep))
304+
width, ok := st.Width()
305+
if !ok {
306+
width = -1
307+
}
308+
prec, ok := st.Precision()
309+
if !ok {
310+
prec = -1
311+
}
312+
flag := ""
313+
for _, b := range "-+# 0" {
314+
if st.Flag(int(b)) {
315+
flag += string(b)
316+
}
317+
}
318+
convs = append(convs, conversion{
319+
verb: verb,
320+
width: width,
321+
prec: prec,
322+
flag: flag,
323+
operand: operand,
324+
})
325+
})
326+
}
327+
328+
// Interleave the literals and the conversions.
329+
var formatItems []formatItem
330+
s := fmt.Sprintf(format, wrappers...)
331+
// All errors begin with the string "%!".
332+
if strings.Contains(s, "%!") {
333+
return nil, fmt.Errorf("%s", strings.Replace(s, sep, "", -1))
334+
}
335+
for i, word := range strings.Split(s, sep) {
336+
if word != "" {
337+
formatItems = append(formatItems, formatLiteral{
338+
literal: word,
339+
rang: posRange{
340+
start: pos,
341+
end: pos + token.Pos(len(word)),
342+
},
343+
})
344+
pos = pos + token.Pos(len(word))
345+
}
346+
if i < len(convs) {
347+
conv := convs[i]
348+
// Collect %.
349+
formatItems = append(formatItems, formatPercent(pos))
350+
pos += 1
351+
// Collect flags.
352+
if flag := conv.flag; flag != "" {
353+
length := token.Pos(len(conv.flag))
354+
formatItems = append(formatItems, formatFlags{
355+
flag: flag,
356+
rang: posRange{
357+
start: pos,
358+
end: pos + length,
359+
},
360+
})
361+
pos += length
362+
}
363+
// Collect width.
364+
if width := conv.width; conv.width != -1 {
365+
length := token.Pos(len(fmt.Sprintf("%d", conv.width)))
366+
formatItems = append(formatItems, formatWidth{
367+
width: width,
368+
rang: posRange{
369+
start: pos,
370+
end: pos + length,
371+
},
372+
})
373+
pos += length
374+
}
375+
// Collect precision, which starts with a dot.
376+
if prec := conv.prec; conv.prec != -1 {
377+
length := token.Pos(len(fmt.Sprintf("%d", conv.prec))) + 1
378+
formatItems = append(formatItems, formatPrec{
379+
prec: prec,
380+
rang: posRange{
381+
start: pos,
382+
end: pos + length,
383+
},
384+
})
385+
pos += length
386+
}
387+
// Collect verb, which must be present.
388+
length := token.Pos(len(string(conv.verb)))
389+
formatItems = append(formatItems, formatVerb{
390+
verb: conv.verb,
391+
rang: posRange{
392+
start: pos,
393+
end: pos + length,
394+
},
395+
operand: conv.operand,
396+
})
397+
pos += length
398+
}
399+
}
400+
return formatItems, nil
401+
}
402+
134403
type posRange struct {
135404
start, end token.Pos
136405
}
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)