Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e1d3c68

Browse files
committedDec 2, 2024·
zz
1 parent 80799d6 commit e1d3c68

File tree

2 files changed

+311
-2
lines changed

2 files changed

+311
-2
lines changed
 

‎gopls/internal/golang/highlight.go

+276-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,19 @@ 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 && basicList.Kind == token.STRING {
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,266 @@ 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+
format := directive.Value
160+
// Give up when encounter '% %', '%%' for simplicity.
161+
// For example:
162+
//
163+
// fmt.Printf("hello % %s, %-2.3d\n", "world", 123)
164+
//
165+
// The implementation of fmt.doPrintf will ignore first two '%'s,
166+
// causing arguments count bigger than placeholders count (2 > 1), producing
167+
// "%!(EXTRA" error string in formatFunc and incorrect highlight range.
168+
//
169+
// fmt.Printf("%% %s, %-2.3d\n", "world", 123)
170+
//
171+
// This case it will not emit errors, but the recording range of parsef is going to
172+
// shift left because two % are interpreted as one %(escaped), so it becomes:
173+
// fmt.Printf("%% %s, %-2.3d\n", "world", 123)
174+
// | | the range will include a whitespace in left of %s
175+
for i := range len(format) {
176+
if format[i] == '%' {
177+
for j := i + 1; j < len(format); j++ {
178+
c := format[j]
179+
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
180+
break
181+
}
182+
if c == '%' {
183+
return
184+
}
185+
}
186+
}
187+
}
188+
189+
// Computation is based on count of '%', when placeholders and variadic arguments missmatch,
190+
// users are most likely completing arguments, so try to highlight any unfinished one.
191+
// Make sure variadic arguments passed to parsef matches correct count of '%'.
192+
expectedVariadicArgs := make([]ast.Expr, strings.Count(format, "%"))
193+
firstVariadic := -1
194+
for i, arg := range call.Args {
195+
if directive == arg {
196+
firstVariadic = i + 1
197+
argsLen := len(call.Args) - i - 1
198+
if argsLen > len(expectedVariadicArgs) {
199+
// Translate from Printf(a0,"%d %d",5, 6, 7) to [5, 6]
200+
copy(expectedVariadicArgs, call.Args[firstVariadic:firstVariadic+len(expectedVariadicArgs)])
201+
} else {
202+
// Translate from Printf(a0,"%d %d %s",5, 6) to [5, 6, nil]
203+
copy(expectedVariadicArgs[:argsLen], call.Args[firstVariadic:])
204+
}
205+
break
206+
}
207+
}
208+
var percent formatPercent
209+
// Get a position-ordered slice describing each directive item.
210+
parsedDirectives := parsef(format, directive.Pos(), expectedVariadicArgs...)
211+
// Cursor in argument.
212+
if pos > directive.End() {
213+
// Which variadic argument cursor sits inside.
214+
for i := firstVariadic; i < len(call.Args); i++ {
215+
if gastutil.NodeContains(call.Args[i], pos) {
216+
// Offset relative to parsedDirectives.
217+
// (Printf(a0,"%d %d %s",5, 6), firstVariadic=2,i=3)
218+
// ^ cursor here
219+
// -> ([5, 6, nil], firstVariadic=1)
220+
// ^
221+
firstVariadic = i - firstVariadic
222+
break
223+
}
224+
}
225+
index := -1
226+
for _, part := range parsedDirectives {
227+
switch part := part.(type) {
228+
case formatPercent:
229+
percent = part
230+
index++
231+
case formatVerb:
232+
if token.Pos(percent).IsValid() {
233+
if index == firstVariadic {
234+
// Placeholders behave like writting values from arguments to themselves,
235+
// so highlight them with Write semantic.
236+
highlightRange(result, token.Pos(percent), part.rang.end, protocol.Write)
237+
highlightRange(result, part.operand.Pos(), part.operand.End(), protocol.Read)
238+
return
239+
}
240+
percent = formatPercent(token.NoPos)
241+
}
242+
}
243+
}
244+
} else {
245+
// Cursor in format string.
246+
for _, part := range parsedDirectives {
247+
switch part := part.(type) {
248+
case formatPercent:
249+
percent = part
250+
case formatVerb:
251+
if token.Pos(percent).IsValid() {
252+
if token.Pos(percent) <= pos && pos <= part.rang.end {
253+
highlightRange(result, token.Pos(percent), part.rang.end, protocol.Write)
254+
if part.operand != nil {
255+
highlightRange(result, part.operand.Pos(), part.operand.End(), protocol.Read)
256+
}
257+
return
258+
}
259+
percent = formatPercent(token.NoPos)
260+
}
261+
}
262+
}
263+
}
264+
}
265+
266+
// Below are formatting directives definitions.
267+
type formatPercent token.Pos
268+
type formatLiteral struct {
269+
literal string
270+
rang posRange
271+
}
272+
type formatFlags struct {
273+
flag string
274+
rang posRange
275+
}
276+
type formatWidth struct {
277+
width int
278+
rang posRange
279+
}
280+
type formatPrec struct {
281+
prec int
282+
rang posRange
283+
}
284+
type formatVerb struct {
285+
verb rune
286+
rang posRange
287+
operand ast.Expr // verb's corresponding operand, may be nil
288+
}
289+
290+
type formatFunc func(fmt.State, rune)
291+
292+
var _ fmt.Formatter = formatFunc(nil)
293+
294+
func (f formatFunc) Format(st fmt.State, verb rune) { f(st, verb) }
295+
296+
// parsef parses a printf-style format string into its constituent components together with
297+
// their position in the source code, including [formatLiteral], formatting directives
298+
// [formatFlags], [formatPrecision], [formatWidth], [formatPrecision], [formatVerb], and its operand.
299+
func parsef(format string, pos token.Pos, args ...ast.Expr) []any {
300+
const sep = "!!!GOPLS_SEP!!!"
301+
// A Conversion represents a single % operation and its operand.
302+
type conversion struct {
303+
verb rune
304+
width int // or -1
305+
prec int // or -1
306+
flag string // some of "-+# 0"
307+
arg ast.Expr
308+
}
309+
var convs []conversion
310+
wrappers := make([]any, len(args))
311+
for i, arg := range args {
312+
wrappers[i] = formatFunc(func(st fmt.State, verb rune) {
313+
io.WriteString(st, sep)
314+
width, ok := st.Width()
315+
if !ok {
316+
width = -1
317+
}
318+
prec, ok := st.Precision()
319+
if !ok {
320+
prec = -1
321+
}
322+
flag := ""
323+
for _, b := range "-+# 0" {
324+
if st.Flag(int(b)) {
325+
flag += string(b)
326+
}
327+
}
328+
convs = append(convs, conversion{
329+
verb: verb,
330+
width: width,
331+
prec: prec,
332+
flag: flag,
333+
arg: arg,
334+
})
335+
})
336+
}
337+
338+
// Interleave the literals and the conversions.
339+
var directives []any
340+
for i, word := range strings.Split(fmt.Sprintf(format, wrappers...), sep) {
341+
if word != "" {
342+
directives = append(directives, formatLiteral{
343+
literal: word,
344+
rang: posRange{
345+
start: pos,
346+
end: pos + token.Pos(len(word)),
347+
},
348+
})
349+
pos = pos + token.Pos(len(word))
350+
}
351+
if i < len(convs) {
352+
conv := convs[i]
353+
// Collect %.
354+
directives = append(directives, formatPercent(pos))
355+
pos += 1
356+
// Collect flags.
357+
if flag := conv.flag; flag != "" {
358+
length := token.Pos(len(conv.flag))
359+
directives = append(directives, formatFlags{
360+
flag: flag,
361+
rang: posRange{
362+
start: pos,
363+
end: pos + length,
364+
},
365+
})
366+
pos += length
367+
}
368+
// Collect width.
369+
if width := conv.width; conv.width != -1 {
370+
length := token.Pos(len(fmt.Sprintf("%d", conv.width)))
371+
directives = append(directives, formatWidth{
372+
width: width,
373+
rang: posRange{
374+
start: pos,
375+
end: pos + length,
376+
},
377+
})
378+
pos += length
379+
}
380+
// Collect precision, which starts with a dot.
381+
if prec := conv.prec; conv.prec != -1 {
382+
length := token.Pos(len(fmt.Sprintf("%d", conv.prec))) + 1
383+
directives = append(directives, formatPrec{
384+
prec: prec,
385+
rang: posRange{
386+
start: pos,
387+
end: pos + length,
388+
},
389+
})
390+
pos += length
391+
}
392+
// Collect verb, which must be present.
393+
length := token.Pos(len(string(conv.verb)))
394+
directives = append(directives, formatVerb{
395+
verb: conv.verb,
396+
rang: posRange{
397+
start: pos,
398+
end: pos + length,
399+
},
400+
operand: conv.arg,
401+
})
402+
pos += length
403+
}
404+
}
405+
return directives
406+
}
407+
134408
type posRange struct {
135409
start, end token.Pos
136410
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
}

0 commit comments

Comments
 (0)
Please sign in to comment.