From 3e4a92e9daa2c626d2f2efe9cd8d4b1200dda141 Mon Sep 17 00:00:00 2001 From: xzb <2598514867@qq.com> Date: Mon, 2 Dec 2024 01:38:27 +0800 Subject: [PATCH] zz --- gopls/internal/golang/highlight.go | 279 +++++++++++++++++- .../testdata/highlight/highlight_printf.txt | 35 +++ 2 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/highlight/highlight_printf.txt diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index 1174ce7f7d4..b00f40a8e3a 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -10,11 +10,14 @@ import ( "go/ast" "go/token" "go/types" + "io" + "strings" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" + gastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/event" ) @@ -49,7 +52,7 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po } } } - result, err := highlightPath(path, pgf.File, pkg.TypesInfo()) + result, err := highlightPath(path, pgf.File, pkg.TypesInfo(), pos) if err != nil { return nil, err } @@ -69,8 +72,20 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po // highlightPath returns ranges to highlight for the given enclosing path, // which should be the result of astutil.PathEnclosingInterval. -func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]protocol.DocumentHighlightKind, error) { +func highlightPath(path []ast.Node, file *ast.File, info *types.Info, pos token.Pos) (map[posRange]protocol.DocumentHighlightKind, error) { result := make(map[posRange]protocol.DocumentHighlightKind) + // Inside a printf-style call? + for _, node := range path { + if call, ok := node.(*ast.CallExpr); ok { + for _, args := range call.Args { + // Only try when pos is in right side of the format String. + if basicList, ok := args.(*ast.BasicLit); ok && basicList.Pos() < pos && + basicList.Kind == token.STRING && strings.Contains(basicList.Value, "%") { + highlightPrintf(basicList, call, pos, result) + } + } + } + } switch node := path[0].(type) { case *ast.BasicLit: // Import path string literal? @@ -131,6 +146,266 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa return result, nil } +// highlightPrintf identifies and highlights the relationships between placeholders +// in a format string and their corresponding variadic arguments in a printf-style +// function call. +// +// For example: +// +// fmt.Printf("Hello %s, you scored %d", name, score) +// +// If the cursor is on %s or name, highlightPrintf will highlight %s as a write operation, +// and name as a read operation. +func highlightPrintf(directive *ast.BasicLit, call *ast.CallExpr, pos token.Pos, result map[posRange]protocol.DocumentHighlightKind) { + format := directive.Value + // Give up when encounter '% %', '%%' for simplicity. + // For example: + // + // fmt.Printf("hello % %s, %-2.3d\n", "world", 123) + // + // The implementation of fmt.doPrintf will ignore first two '%'s, + // causing arguments count bigger than placeholders count (2 > 1), producing + // "%!(EXTRA" error string in formatFunc and incorrect highlight range. + // + // fmt.Printf("%% %s, %-2.3d\n", "world", 123) + // + // This case it will not emit errors, but the recording range of parsef is going to + // shift left because two % are interpreted as one %(escaped), so it becomes: + // fmt.Printf("%% %s, %-2.3d\n", "world", 123) + // | | the range will include a whitespace in left of %s + for i := range len(format) { + if format[i] == '%' { + for j := i + 1; j < len(format); j++ { + c := format[j] + if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + break + } + if c == '%' { + return + } + } + } + } + + // Computation is based on count of '%', when placeholders and variadic arguments missmatch, + // users are most likely completing arguments, so try to highlight any unfinished one. + // Make sure variadic arguments passed to parsef matches correct count of '%'. + expectedVariadicArgs := make([]ast.Expr, strings.Count(format, "%")) + firstVariadic := -1 + for i, arg := range call.Args { + if directive == arg { + firstVariadic = i + 1 + argsLen := len(call.Args) - i - 1 + if argsLen > len(expectedVariadicArgs) { + // Translate from Printf(a0,"%d %d",5, 6, 7) to [5, 6] + copy(expectedVariadicArgs, call.Args[firstVariadic:firstVariadic+len(expectedVariadicArgs)]) + } else { + // Translate from Printf(a0,"%d %d %s",5, 6) to [5, 6, nil] + copy(expectedVariadicArgs[:argsLen], call.Args[firstVariadic:]) + } + break + } + } + var percent formatPercent + // Get a position-ordered slice describing each directive item. + parsedDirectives := parsef(format, directive.Pos(), expectedVariadicArgs...) + // Cursor in argument. + if pos > directive.End() { + // Which variadic argument cursor sits inside. + for i := firstVariadic; i < len(call.Args); i++ { + if gastutil.NodeContains(call.Args[i], pos) { + // Offset relative to parsedDirectives. + // (Printf(a0,"%d %d %s",5, 6), firstVariadic=2,i=3) + // ^ cursor here + // -> ([5, 6, nil], firstVariadic=1) + // ^ + firstVariadic = i - firstVariadic + break + } + } + index := -1 + for _, part := range parsedDirectives { + switch part := part.(type) { + case formatPercent: + percent = part + index++ + case formatVerb: + if token.Pos(percent).IsValid() { + if index == firstVariadic { + // Placeholders behave like writting values from arguments to themselves, + // so highlight them with Write semantic. + highlightRange(result, token.Pos(percent), part.rang.end, protocol.Write) + highlightRange(result, part.operand.Pos(), part.operand.End(), protocol.Read) + return + } + percent = formatPercent(token.NoPos) + } + } + } + } else { + // Cursor in format string. + for _, part := range parsedDirectives { + switch part := part.(type) { + case formatPercent: + percent = part + case formatVerb: + if token.Pos(percent).IsValid() { + if token.Pos(percent) <= pos && pos <= part.rang.end { + highlightRange(result, token.Pos(percent), part.rang.end, protocol.Write) + if part.operand != nil { + highlightRange(result, part.operand.Pos(), part.operand.End(), protocol.Read) + } + return + } + percent = formatPercent(token.NoPos) + } + } + } + } +} + +// Below are formatting directives definitions. +type formatPercent token.Pos +type formatLiteral struct { + literal string + rang posRange +} +type formatFlags struct { + flag string + rang posRange +} +type formatWidth struct { + width int + rang posRange +} +type formatPrec struct { + prec int + rang posRange +} +type formatVerb struct { + verb rune + rang posRange + operand ast.Expr // verb's corresponding operand, may be nil +} + +type formatFunc func(fmt.State, rune) + +var _ fmt.Formatter = formatFunc(nil) + +func (f formatFunc) Format(st fmt.State, verb rune) { f(st, verb) } + +// parsef parses a printf-style format string into its constituent components together with +// their position in the source code, including [formatLiteral], formatting directives +// [formatFlags], [formatPrecision], [formatWidth], [formatPrecision], [formatVerb], and its operand. +func parsef(format string, pos token.Pos, args ...ast.Expr) []any { + const sep = "!!!GOPLS_SEP!!!" + // A Conversion represents a single % operation and its operand. + type conversion struct { + verb rune + width int // or -1 + prec int // or -1 + flag string // some of "-+# 0" + operand ast.Expr + } + var convs []conversion + wrappers := make([]any, len(args)) + for i, operand := range args { + wrappers[i] = formatFunc(func(st fmt.State, verb rune) { + io.WriteString(st, sep) + width, ok := st.Width() + if !ok { + width = -1 + } + prec, ok := st.Precision() + if !ok { + prec = -1 + } + flag := "" + for _, b := range "-+# 0" { + if st.Flag(int(b)) { + flag += string(b) + } + } + convs = append(convs, conversion{ + verb: verb, + width: width, + prec: prec, + flag: flag, + operand: operand, + }) + }) + } + + // Interleave the literals and the conversions. + var directives []any + for i, word := range strings.Split(fmt.Sprintf(format, wrappers...), sep) { + if word != "" { + directives = append(directives, formatLiteral{ + literal: word, + rang: posRange{ + start: pos, + end: pos + token.Pos(len(word)), + }, + }) + pos = pos + token.Pos(len(word)) + } + if i < len(convs) { + conv := convs[i] + // Collect %. + directives = append(directives, formatPercent(pos)) + pos += 1 + // Collect flags. + if flag := conv.flag; flag != "" { + length := token.Pos(len(conv.flag)) + directives = append(directives, formatFlags{ + flag: flag, + rang: posRange{ + start: pos, + end: pos + length, + }, + }) + pos += length + } + // Collect width. + if width := conv.width; conv.width != -1 { + length := token.Pos(len(fmt.Sprintf("%d", conv.width))) + directives = append(directives, formatWidth{ + width: width, + rang: posRange{ + start: pos, + end: pos + length, + }, + }) + pos += length + } + // Collect precision, which starts with a dot. + if prec := conv.prec; conv.prec != -1 { + length := token.Pos(len(fmt.Sprintf("%d", conv.prec))) + 1 + directives = append(directives, formatPrec{ + prec: prec, + rang: posRange{ + start: pos, + end: pos + length, + }, + }) + pos += length + } + // Collect verb, which must be present. + length := token.Pos(len(string(conv.verb))) + directives = append(directives, formatVerb{ + verb: conv.verb, + rang: posRange{ + start: pos, + end: pos + length, + }, + operand: conv.operand, + }) + pos += length + } + } + return directives +} + type posRange struct { start, end token.Pos } diff --git a/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt b/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt new file mode 100644 index 00000000000..156b1392587 --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt @@ -0,0 +1,35 @@ +This test checks functionality of the printf-like directives and operands highlight. + +-- flags -- +-ignore_extra_diags + +-- highlights.go -- +package highlightprintf + +import ( + "fmt" +) + +func BasicPrintfHighlights() { + fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normals, "%s", write),hiloc(normalarg0, "\"Alice\"", read),highlightall(normals, normalarg0) + fmt.Printf("Hello %s, you have %d new messages!", "Alice", 5) //@hiloc(normald, "%d", write),hiloc(normalargs1, "5", read),highlightall(normald, normalargs1) +} + +func ComplexPrintfHighlights() { + 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) + 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) +} + +func MissingDirectives() { + fmt.Printf("Hello %s, you have 5 new messages!", "Alice", 5) //@hiloc(missings, "%s", write),hiloc(missingargs0, "\"Alice\"", read),highlightall(missings, missingargs0) +} + +func TooManyDirectives() { + fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanys, "%s", write),hiloc(toomanyargs0, "\"Alice\"", read),highlightall(toomanys, toomanyargs0) + fmt.Printf("Hello %s, you have %d new %s %q messages!", "Alice", 5) //@hiloc(toomanyd, "%d", write),hiloc(toomanyargs1, "5", read),highlightall(toomanyd, toomanyargs1) +} + +func SpecialChars() { + 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) + 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) +}