Skip to content

Commit 746a00e

Browse files
committed
zz
1 parent 80799d6 commit 746a00e

File tree

2 files changed

+310
-2
lines changed

2 files changed

+310
-2
lines changed

gopls/internal/golang/highlight.go

+280-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 && gastutil.NodeContains(call, pos) {
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,270 @@ 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+
if firstVariadic == -1 {
209+
// No argument at right of directive.
210+
return
211+
}
212+
var percent formatPercent
213+
// Get a position-ordered slice describing each directive item.
214+
parsedDirectives := parsef(format, directive.Pos(), expectedVariadicArgs...)
215+
// Cursor in argument.
216+
if pos > directive.End() {
217+
// Which variadic argument cursor sits inside.
218+
for i := firstVariadic; i < len(call.Args); i++ {
219+
if gastutil.NodeContains(call.Args[i], pos) {
220+
// Offset relative to parsedDirectives.
221+
// (Printf(a0,"%d %d %s",5, 6), firstVariadic=2,i=3)
222+
// ^ cursor here
223+
// -> ([5, 6, nil], firstVariadic=1)
224+
// ^
225+
firstVariadic = i - firstVariadic
226+
break
227+
}
228+
}
229+
index := -1
230+
for _, part := range parsedDirectives {
231+
switch part := part.(type) {
232+
case formatPercent:
233+
percent = part
234+
index++
235+
case formatVerb:
236+
if token.Pos(percent).IsValid() {
237+
if index == firstVariadic {
238+
// Placeholders behave like writting values from arguments to themselves,
239+
// so highlight them with Write semantic.
240+
highlightRange(result, token.Pos(percent), part.rang.end, protocol.Write)
241+
highlightRange(result, part.arg.Pos(), part.arg.End(), protocol.Read)
242+
return
243+
}
244+
percent = formatPercent(token.NoPos)
245+
}
246+
}
247+
}
248+
} else {
249+
// Cursor in format string.
250+
for _, part := range parsedDirectives {
251+
switch part := part.(type) {
252+
case formatPercent:
253+
percent = part
254+
case formatVerb:
255+
if token.Pos(percent).IsValid() {
256+
if token.Pos(percent) <= pos && pos <= part.rang.end {
257+
highlightRange(result, token.Pos(percent), part.rang.end, protocol.Write)
258+
if part.arg != nil {
259+
highlightRange(result, part.arg.Pos(), part.arg.End(), protocol.Read)
260+
}
261+
return
262+
}
263+
percent = formatPercent(token.NoPos)
264+
}
265+
}
266+
}
267+
}
268+
}
269+
270+
// Below are formatting directives definitions.
271+
type formatPercent token.Pos
272+
type formatLiteral struct {
273+
literal string
274+
rang posRange
275+
}
276+
type formatFlags struct {
277+
flag string
278+
rang posRange
279+
}
280+
type formatWidth struct {
281+
width int
282+
rang posRange
283+
}
284+
type formatPrec struct {
285+
prec int
286+
rang posRange
287+
}
288+
type formatVerb struct {
289+
verb rune
290+
rang posRange
291+
arg ast.Expr // may be nil
292+
}
293+
294+
type formatFunc func(fmt.State, rune)
295+
296+
var _ fmt.Formatter = formatFunc(nil)
297+
298+
func (f formatFunc) Format(st fmt.State, verb rune) { f(st, verb) }
299+
300+
// parsef parses a printf-style format string into its constituent components together with
301+
// their position in the source code, including literals, formatting directives
302+
// (flags, width, precision, verb), and its operand.
303+
func parsef(format string, pos token.Pos, args ...ast.Expr) []any {
304+
const sep = "!!!GOPLS_SEP!!!"
305+
// A Conversion represents a single % operation and its operand.
306+
type conversion struct {
307+
verb rune
308+
width int // or -1
309+
prec int // or -1
310+
flag string // some of "-+# 0"
311+
arg ast.Expr
312+
}
313+
var convs []conversion
314+
wrappers := make([]any, len(args))
315+
for i, arg := range args {
316+
wrappers[i] = formatFunc(func(st fmt.State, verb rune) {
317+
io.WriteString(st, sep)
318+
width, ok := st.Width()
319+
if !ok {
320+
width = -1
321+
}
322+
prec, ok := st.Precision()
323+
if !ok {
324+
prec = -1
325+
}
326+
flag := ""
327+
for _, b := range "-+# 0" {
328+
if st.Flag(int(b)) {
329+
flag += string(b)
330+
}
331+
}
332+
convs = append(convs, conversion{
333+
verb: verb,
334+
width: width,
335+
prec: prec,
336+
flag: flag,
337+
arg: arg,
338+
})
339+
})
340+
}
341+
342+
// Interleave the literals and the conversions.
343+
var directives []any
344+
for i, word := range strings.Split(fmt.Sprintf(format, wrappers...), sep) {
345+
if word != "" {
346+
directives = append(directives, formatLiteral{
347+
literal: word,
348+
rang: posRange{
349+
start: pos,
350+
end: pos + token.Pos(len(word)),
351+
},
352+
})
353+
pos = pos + token.Pos(len(word))
354+
}
355+
if i < len(convs) {
356+
conv := convs[i]
357+
// Collect %.
358+
directives = append(directives, formatPercent(pos))
359+
pos += 1
360+
// Collect flags.
361+
if flag := conv.flag; flag != "" {
362+
length := token.Pos(len(conv.flag))
363+
directives = append(directives, formatFlags{
364+
flag: flag,
365+
rang: posRange{
366+
start: pos,
367+
end: pos + length,
368+
},
369+
})
370+
pos += length
371+
}
372+
// Collect width.
373+
if width := conv.width; conv.width != -1 {
374+
length := token.Pos(len(fmt.Sprintf("%d", conv.width)))
375+
directives = append(directives, formatWidth{
376+
width: width,
377+
rang: posRange{
378+
start: pos,
379+
end: pos + length,
380+
},
381+
})
382+
pos += length
383+
}
384+
// Collect precision, which starts with a dot.
385+
if prec := conv.prec; conv.prec != -1 {
386+
length := token.Pos(len(fmt.Sprintf("%d", conv.prec))) + 1
387+
directives = append(directives, formatPrec{
388+
prec: prec,
389+
rang: posRange{
390+
start: pos,
391+
end: pos + length,
392+
},
393+
})
394+
pos += length
395+
}
396+
// Collect verb, which must be present.
397+
length := token.Pos(len(string(conv.verb)))
398+
directives = append(directives, formatVerb{
399+
verb: conv.verb,
400+
rang: posRange{
401+
start: pos,
402+
end: pos + length,
403+
},
404+
arg: conv.arg,
405+
})
406+
pos += length
407+
}
408+
}
409+
return directives
410+
}
411+
134412
type posRange struct {
135413
start, end token.Pos
136414
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 MissingDirectives() {
19+
fmt.Printf("Hello %s, you have 5 new messages!", "Alice", 5) //@hiloc(missings, "%s", write),hiloc(missingargs0, "\"Alice\"", read),highlightall(missings, missingargs0)
20+
}
21+
22+
func TooManyDirectives() {
23+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanys, "%s", write),hiloc(toomanyargs0, "\"Alice\"", read),highlightall(toomanys, toomanyargs0)
24+
fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanyd, "%d", write),hiloc(toomanyargs1, "5", read),highlightall(toomanyd, toomanyargs1)
25+
}
26+
27+
func SpecialChars() {
28+
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)
29+
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)
30+
}

0 commit comments

Comments
 (0)