From 746a00e8ac3770f58d34ad50751f3cba1d6f2fe8 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 | 282 +++++++++++++++++- .../testdata/highlight/highlight_printf.txt | 30 ++ 2 files changed, 310 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..bf8bfc83faa 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,19 @@ 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 && gastutil.NodeContains(call, pos) { + 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 { + highlightPrintf(basicList, call, pos, result) + } + } + } + } switch node := path[0].(type) { case *ast.BasicLit: // Import path string literal? @@ -131,6 +145,270 @@ 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 + } + } + if firstVariadic == -1 { + // No argument at right of directive. + return + } + 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.arg.Pos(), part.arg.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.arg != nil { + highlightRange(result, part.arg.Pos(), part.arg.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 + arg ast.Expr // 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 literals, formatting directives +// (flags, width, precision, verb), 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" + arg ast.Expr + } + var convs []conversion + wrappers := make([]any, len(args)) + for i, arg := 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, + arg: arg, + }) + }) + } + + // 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, + }, + arg: conv.arg, + }) + 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..9ddc5ac590c --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt @@ -0,0 +1,30 @@ +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 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) +}