Skip to content

Commit

Permalink
zz
Browse files Browse the repository at this point in the history
  • Loading branch information
xzbdmw committed Dec 5, 2024
1 parent 2f9c834 commit 1f760f1
Show file tree
Hide file tree
Showing 2 changed files with 320 additions and 2 deletions.
282 changes: 280 additions & 2 deletions gopls/internal/golang/highlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand All @@ -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?
Expand Down Expand Up @@ -131,6 +146,269 @@ 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) {
// Two '%'s are interpreted as one '%'(escaped), let's replace them with spaces.
format := strings.Replace(directive.Value, "%%", " ", -1)
// Give up when encounter '% %'.
// 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.
for i := 0; i < len(format); i++ {
if format[i] == '%' {
for j := i + 1; j < len(format); j++ {
c := format[j]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
i = j
break
}
if c == '%' {
return
}
}
}
}

// 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
formatItems := parsef(format, directive.Pos(), expectedVariadicArgs...)
// Cursor in argument.
if pos > directive.End() {
var curVariadic int
// Which variadic argument cursor sits inside.
for i := firstVariadic; i < len(call.Args); i++ {
if gastutil.NodeContains(call.Args[i], pos) {
// Offset relative to formatItems.
curVariadic = i - firstVariadic
break
}
}
index := -1
for _, item := range formatItems {
switch item := item.(type) {
case formatPercent:
percent = item
index++
case formatVerb:
if token.Pos(percent).IsValid() {
if index == curVariadic {
// Placeholders behave like writting values from arguments to themselves,
// so highlight them with Write semantic.
highlightRange(result, token.Pos(percent), item.rang.end, protocol.Write)
highlightRange(result, item.operand.Pos(), item.operand.End(), protocol.Read)
return
}
percent = formatPercent(token.NoPos)
}
}
}
} else {
// Cursor in format string.
for _, item := range formatItems {
switch item := item.(type) {
case formatPercent:
percent = item
case formatVerb:
if token.Pos(percent).IsValid() {
if token.Pos(percent) <= pos && pos <= item.rang.end {
highlightRange(result, token.Pos(percent), item.rang.end, protocol.Write)
if item.operand != nil {
highlightRange(result, item.operand.Pos(), item.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 formatItem interface {
formatItem()
}

func (formatPercent) formatItem() {}
func (formatLiteral) formatItem() {}
func (formatVerb) formatItem() {}
func (formatWidth) formatItem() {}
func (formatFlags) formatItem() {}
func (formatPrec) formatItem() {}

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.
//
// If format contains explicit argument indexes, eg. fmt.Sprintf("%[2]d %[1]d\n", 11, 22),
// or directives count does not match argument count, the returned range will not be correct.
func parsef(format string, pos token.Pos, args ...ast.Expr) []formatItem {
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 formatItems []formatItem
for i, word := range strings.Split(fmt.Sprintf(format, wrappers...), sep) {
if word != "" {
formatItems = append(formatItems, 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 %.
formatItems = append(formatItems, formatPercent(pos))
pos += 1
// Collect flags.
if flag := conv.flag; flag != "" {
length := token.Pos(len(conv.flag))
formatItems = append(formatItems, 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)))
formatItems = append(formatItems, 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
formatItems = append(formatItems, 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)))
formatItems = append(formatItems, formatVerb{
verb: conv.verb,
rang: posRange{
start: pos,
end: pos + length,
},
operand: conv.operand,
})
pos += length
}
}
return formatItems
}

type posRange struct {
start, end token.Pos
}
Expand Down
40 changes: 40 additions & 0 deletions gopls/internal/test/marker/testdata/highlight/highlight_printf.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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)
}

func Escaped() {
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)
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)
}

0 comments on commit 1f760f1

Please sign in to comment.