diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 132cf7ef5fa..abc9275fd10 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -486,8 +486,7 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, name string } return } - // Check formats against args. - states, err := fmtstr.ParsePrintf(pass.TypesInfo, call) + directives, err := fmtstr.ParsePrintf(pass.TypesInfo, call) if err != nil { pass.ReportRangef(call.Fun, "%s %s", name, err.Error()) return @@ -495,16 +494,17 @@ func checkPrintf(pass *analysis.Pass, kind Kind, call *ast.CallExpr, name string maxArgNum := firstArg anyIndex := false - for _, state := range states { - if (state.Prec != nil && state.Prec.Index != -1) || - (state.Width != nil && state.Width.Index != -1) || - (state.Verb != nil && state.Verb.Index != -1) { + // Check formats against args. + for _, directive := range directives { + if (directive.Prec != nil && directive.Prec.Index != -1) || + (directive.Width != nil && directive.Width.Index != -1) || + (directive.Verb != nil && directive.Verb.Index != -1) { anyIndex = true } - if !okPrintfArg(pass, call, &maxArgNum, name, state) { // One error per format is enough. + if !okPrintfArg(pass, call, &maxArgNum, name, directive) { // One error per format is enough. return } - if state.Verb.Verb == 'w' { + if directive.Verb.Verb == 'w' { switch kind { case KindNone, KindPrint, KindPrintf: pass.Reportf(call.Pos(), "%s does not support error-wrapping directive %%w", name) @@ -591,8 +591,8 @@ var printVerbs = []printVerb{ // okPrintfArg compares the FormatDirective to the arguments actually present, // reporting any discrepancies it can discern. If the final argument is ellipsissed, // there's little it can do for that. -func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, maxArgNum *int, name string, state *fmtstr.FormatDirective) (ok bool) { - verb := state.Verb.Verb +func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, maxArgNum *int, name string, directive *fmtstr.FormatDirective) (ok bool) { + verb := directive.Verb.Verb var v printVerb found := false // Linear scan is fast enough for a small list. @@ -606,42 +606,42 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, maxArgNum *int, name s // Could verb's arg implement fmt.Formatter? // Skip check for the %w verb, which requires an error. formatter := false - if v.typ != argError && state.Verb.ArgNum < len(call.Args) { - if tv, ok := pass.TypesInfo.Types[call.Args[state.Verb.ArgNum]]; ok { + if v.typ != argError && directive.Verb.ArgNum < len(call.Args) { + if tv, ok := pass.TypesInfo.Types[call.Args[directive.Verb.ArgNum]]; ok { formatter = isFormatter(tv.Type) } } if !formatter { if !found { - pass.ReportRangef(call, "%s format %s has unknown verb %c", name, state.Format, verb) + pass.ReportRangef(call, "%s format %s has unknown verb %c", name, directive.Format, verb) return false } - for _, flag := range state.Flags { + for _, flag := range directive.Flags { // TODO: Disable complaint about '0' for Go 1.10. To be fixed properly in 1.11. // See issues 23598 and 23605. if flag == '0' { continue } if !strings.ContainsRune(v.flags, rune(flag)) { - pass.ReportRangef(call, "%s format %s has unrecognized flag %c", name, state.Format, flag) + pass.ReportRangef(call, "%s format %s has unrecognized flag %c", name, directive.Format, flag) return false } } } var argNums []int - if state.Width != nil && state.Width.ArgNum != -1 { - argNums = append(argNums, state.Width.ArgNum) + if directive.Width != nil && directive.Width.ArgNum != -1 { + argNums = append(argNums, directive.Width.ArgNum) } - if state.Prec != nil && state.Prec.ArgNum != -1 { - argNums = append(argNums, state.Prec.ArgNum) + if directive.Prec != nil && directive.Prec.ArgNum != -1 { + argNums = append(argNums, directive.Prec.ArgNum) } // Verb is good. If len(argNums)>0, we have something like %.*s and all // args in argNums must be an integer. for i := 0; i < len(argNums); i++ { argNum := argNums[i] - if !argCanBeChecked(pass, call, argNums[i], state, name) { + if !argCanBeChecked(pass, call, argNums[i], directive, name) { return } arg := call.Args[argNum] @@ -650,14 +650,14 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, maxArgNum *int, name s if reason != "" { details = " (" + reason + ")" } - pass.ReportRangef(call, "%s format %s uses non-int %s%s as argument of *", name, state.Format, analysisutil.Format(pass.Fset, arg), details) + pass.ReportRangef(call, "%s format %s uses non-int %s%s as argument of *", name, directive.Format, analysisutil.Format(pass.Fset, arg), details) return false } } - // Collect to conveniently update maxArgNum. - if state.Verb != nil && state.Verb.ArgNum != -1 && verb != '%' { - argNums = append(argNums, state.Verb.ArgNum) + // Collect to update maxArgNum in one loop. + if directive.Verb != nil && directive.Verb.ArgNum != -1 && verb != '%' { + argNums = append(argNums, directive.Verb.ArgNum) } for _, n := range argNums { if n >= *maxArgNum { @@ -671,12 +671,12 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, maxArgNum *int, name s // Now check verb's type. argNum := argNums[len(argNums)-1] - if !argCanBeChecked(pass, call, argNums[len(argNums)-1], state, name) { + if !argCanBeChecked(pass, call, argNums[len(argNums)-1], directive, name) { return false } arg := call.Args[argNum] if isFunctionValue(pass, arg) && verb != 'p' && verb != 'T' { - pass.ReportRangef(call, "%s format %s arg %s is a func value, not called", name, state.Format, analysisutil.Format(pass.Fset, arg)) + pass.ReportRangef(call, "%s format %s arg %s is a func value, not called", name, directive.Format, analysisutil.Format(pass.Fset, arg)) return false } if reason, ok := matchArgType(pass, v.typ, arg); !ok { @@ -688,12 +688,12 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, maxArgNum *int, name s if reason != "" { details = " (" + reason + ")" } - pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s", name, state.Format, analysisutil.Format(pass.Fset, arg), typeString, details) + pass.ReportRangef(call, "%s format %s has arg %s of wrong type %s%s", name, directive.Format, analysisutil.Format(pass.Fset, arg), typeString, details) return false } - if v.typ&argString != 0 && v.verb != 'T' && !bytes.Contains(state.Flags, []byte{'#'}) { + if v.typ&argString != 0 && v.verb != 'T' && !bytes.Contains(directive.Flags, []byte{'#'}) { if methodName, ok := recursiveStringer(pass, arg); ok { - pass.ReportRangef(call, "%s format %s with arg %s causes recursive %s method call", name, state.Format, analysisutil.Format(pass.Fset, arg), methodName) + pass.ReportRangef(call, "%s format %s with arg %s causes recursive %s method call", name, directive.Format, analysisutil.Format(pass.Fset, arg), methodName) return false } } @@ -777,7 +777,7 @@ func isFunctionValue(pass *analysis.Pass, e ast.Expr) bool { // argCanBeChecked reports whether the specified argument is statically present; // it may be beyond the list of arguments or in a terminal slice... argument, which // means we can't see it. -func argCanBeChecked(pass *analysis.Pass, call *ast.CallExpr, argNum int, state *fmtstr.FormatDirective, name string) bool { +func argCanBeChecked(pass *analysis.Pass, call *ast.CallExpr, argNum int, directive *fmtstr.FormatDirective, name string) bool { if argNum <= 0 { // Shouldn't happen, so catch it with prejudice. panic("negative arg num") @@ -793,8 +793,8 @@ func argCanBeChecked(pass *analysis.Pass, call *ast.CallExpr, argNum int, state } // There are bad indexes in the format or there are fewer arguments than the format needs. // This is the argument number relative to the format: Printf("%s", "hi") will give 1 for the "hi". - arg := argNum - state.FirstArg + 1 // People think of arguments as 1-indexed. - pass.ReportRangef(call, "%s format %s reads arg #%d, but call has %v", name, state.Format, arg, count(len(call.Args)-state.FirstArg, "arg")) + arg := argNum - directive.FirstArg + 1 // People think of arguments as 1-indexed. + pass.ReportRangef(call, "%s format %s reads arg #%d, but call has %v", name, directive.Format, arg, count(len(call.Args)-directive.FirstArg, "arg")) return false } diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index bb6773c5074..3aa875a7c4a 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -16,8 +16,8 @@ import ( "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" + "golang.org/x/tools/internal/fmtstr" ) func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.DocumentHighlight, error) { @@ -73,18 +73,21 @@ func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, po // which should be the result of astutil.PathEnclosingInterval. 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? + + // Inside a printf-style call, printf("...%v...", arg)? + // Treat each corresponding ("%v", arg) pair as a highlight class. 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) + idx := fmtstr.FormatStringIndex(info, call) + if idx >= 0 && idx < len(call.Args) { + format, ok := fmtstr.StringConstantExpr(info, call.Args[idx]) + if ok && strings.Contains(format, "%") { + highlightPrintf(info, call, call.Args[idx].Pos(), pos, result) } } } } + switch node := path[0].(type) { case *ast.BasicLit: // Import path string literal? @@ -145,259 +148,74 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info, pos token. 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. -// +// highlightPrintf highlights directives 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) - if strings.Contains(directive.Value, "%[") || - strings.Contains(directive.Value, "%p") || - strings.Contains(directive.Value, "%T") { - // parsef can not handle these cases. - return - } - 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 - } - } - formatItems, err := parsef(format, directive.Pos(), expectedVariadicArgs...) +func highlightPrintf(info *types.Info, call *ast.CallExpr, formatPos token.Pos, cursorPos token.Pos, result map[posRange]protocol.DocumentHighlightKind) { + directives, err := fmtstr.ParsePrintf(info, call) if err != nil { return } - var percent formatPercent - // 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) - } - } + + highlight := func(start, end token.Pos, argNum int) bool { + var ( + rangeStart = formatPos + token.Pos(start) + 1 // add offset for leading '"' + rangeEnd = formatPos + token.Pos(end) + 1 // add offset for leading '"' + arg ast.Expr // may not exist + ) + if len(call.Args) > argNum { + arg = call.Args[argNum] } - } 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) - } + + // Highlight the directive and its potential argument if the cursor is within etheir range. + if (cursorPos >= rangeStart && cursorPos < rangeEnd) || (arg != nil && cursorPos >= arg.Pos() && cursorPos < arg.End()) { + highlightRange(result, rangeStart, rangeEnd, protocol.Write) + if arg != nil { + highlightRange(result, arg.Pos(), arg.End(), protocol.Read) } + return true } + return false } -} - -// 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) + // If width or prec has any *, we can not highlight the full range from % to verb, + // because it will overlap with the sub-range of *, for example: + // + // fmt.Printf("%*[3]d", 4, 5, 6) + // ^ ^ we can only highlight this range when cursor in 6. '*' as a one-rune range will + // highlight for 4. + anyAsterisk := false + for _, directive := range directives { + width, prec, verb := directive.Width, directive.Prec, directive.Verb + if (prec != nil && prec.Kind != fmtstr.Literal) || + (width != nil && width.Kind != fmtstr.Literal) { + anyAsterisk = true + } -func (f formatFunc) Format(st fmt.State, verb rune) { f(st, verb) } + // Try highlight Width. + if width != nil && width.ArgNum != -1 && highlight(token.Pos(width.Range.Start), token.Pos(width.Range.End), width.ArgNum) { + return + } -// 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), -// the returned range will not be correct. -// If an invalid argument is given for a verb, such as providing a string to %d, the returned error will -// contain a description of the problem. -func parsef(format string, pos token.Pos, args ...ast.Expr) ([]formatItem, error) { - 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) { - st.Write([]byte(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, - }) - }) - } + // Try highlight Prec. + if prec != nil && prec.ArgNum != -1 && highlight(token.Pos(prec.Range.Start), token.Pos(prec.Range.End), prec.ArgNum) { + return + } - // Interleave the literals and the conversions. - var formatItems []formatItem - s := fmt.Sprintf(format, wrappers...) - // All errors begin with the string "%!". - if strings.Contains(s, "%!") { - return nil, fmt.Errorf("%s", strings.Replace(s, sep, "", -1)) - } - for i, word := range strings.Split(s, 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 + // Try highlight Verb. + if verb.Verb != '%' { + if anyAsterisk && highlight(token.Pos(verb.Range.Start), token.Pos(verb.Range.End), verb.ArgNum) { + return + } else if highlight(token.Pos(directive.Range.Start), token.Pos(directive.Range.End), verb.ArgNum) { + return } - // 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, nil } type posRange struct { diff --git a/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt b/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt index ea0011be205..e954ef4facb 100644 --- a/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt +++ b/gopls/internal/test/marker/testdata/highlight/highlight_printf.txt @@ -29,12 +29,13 @@ func TooManyDirectives() { 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 VerbIsPercentage() { + fmt.Printf("%4.2% %d", 6) //@hiloc(z1, "%d", write),hiloc(z2, "6", read),highlightall(z1, z2) } -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) +func Indexed() { + fmt.Printf("%[1]d", 3) //@hiloc(i1, "%[1]d", write),hiloc(i2, "3", read),highlightall(i1, i2) + fmt.Printf("%[1]*d", 3, 6) //@hiloc(i3, "[1]*", write),hiloc(i4, "3", read),hiloc(i5, "d", write),hiloc(i6, "6", read),highlightall(i3, i4),highlightall(i5, i6) + fmt.Printf("%[2]*[1]d", 3, 4) //@hiloc(i7, "[2]*", write),hiloc(i8, "4", read),hiloc(i9, "[1]d", write),hiloc(i10, "3", read),highlightall(i7, i8),highlightall(i9, i10) + fmt.Printf("%[2]*.[1]*[3]d", 4, 5, 6) //@hiloc(i11, "[2]*", write),hiloc(i12, "5", read),hiloc(i13, ".[1]*", write),hiloc(i14, "4", read),hiloc(i15, "[3]d", write),hiloc(i16, "6", read),highlightall(i11, i12),highlightall(i13, i14),highlightall(i15, i16) } diff --git a/internal/fmtstr/parse.go b/internal/fmtstr/parse.go index 92c46423c02..c24cd7eec46 100644 --- a/internal/fmtstr/parse.go +++ b/internal/fmtstr/parse.go @@ -51,11 +51,11 @@ func ParsePrintf(info *types.Info, call *ast.CallExpr) ([]*FormatDirective, erro return nil, err } - state.addRange(i) + state.addOffset(i) states = append(states, state) w = len(state.Format) - // Do not waste an augument for '%'. + // Do not waste an argument for '%'. if state.Verb.Verb != '%' { argNum = state.argNum + 1 } @@ -68,7 +68,7 @@ func ParsePrintf(info *types.Info, call *ast.CallExpr) ([]*FormatDirective, erro // if the directive is malformed. The firstArg and argNum parameters help determine how // arguments map to this directive. // -// Parse sequence: '%' -> flags? -> index? -> width(*)? -> '.'? -> index? -> precision(*)? -> index? -> verb. +// Parse sequence: '%' -> flags -> {[N]* or width} -> .{[N]* or precision} -> [N] -> verb. func parsePrintfVerb(call *ast.CallExpr, format string, firstArg, argNum int) (*FormatDirective, error) { state := &FormatDirective{ Format: format, @@ -190,23 +190,22 @@ type FormatDirective struct { nbytes int } -// Type of size specifier encountered for width or precision. type SizeKind int const ( - Literal SizeKind = iota // A literal number, e.g. "4" in "%4d" - Star // A dynamic size from an argument, e.g. "%*d" - IndexedStar // A dynamic size with an explicit index, e.g. "%[2]*d" + Literal SizeKind = iota // A literal number, e.g. "4" in "%4d" + Asterisk // A dynamic size from an argument, e.g. "%*d" + IndexedAsterisk // A dynamic size with an explicit index, e.g. "%[2]*d" ) // DirectiveSize describes a width or precision in a format directive. -// Depending on Kind, it may represent a literal number, a star, or an indexed star. +// Depending on Kind, it may represent a literal number, a asterisk, or an indexed asterisk. type DirectiveSize struct { Kind SizeKind // Type of size specifier Range posRange // Position of the size specifier within the directive Size int // The literal size if Kind == Literal, otherwise -1 - Index int // If Kind == IndexedStar, the argument index used to obtain the size - ArgNum int // The argument position if Kind == Star or IndexedStar, relative to CallExpr. + Index int // If Kind == IndexedAsterisk, the argument index used to obtain the size + ArgNum int // The argument position if Kind == Asterisk or IndexedAsterisk, relative to CallExpr. } // DirectiveVerb represents the verb character of a format directive (e.g., 'd', 's', 'f'). @@ -222,9 +221,9 @@ type posRange struct { Start, End int } -// addRange adjusts the recorded positions in Verb, Width, Prec, and the +// addOffset adjusts the recorded positions in Verb, Width, Prec, and the // directive's overall Range to be relative to the position in the full format string. -func (s *FormatDirective) addRange(parsedLen int) { +func (s *FormatDirective) addOffset(parsedLen int) { if s.Verb != nil { s.Verb.Range.Start += parsedLen s.Verb.Range.End += parsedLen @@ -287,6 +286,7 @@ func (s *FormatDirective) parseIndex() error { s.index = num s.indexPos = start - 1 } + ok := true if s.nbytes == len(s.Format) || s.nbytes == start || s.Format[s.nbytes] != ']' { ok = false // syntax error is either missing "]" or invalid index. @@ -300,6 +300,7 @@ func (s *FormatDirective) parseIndex() error { if err != nil || !ok || arg32 <= 0 || arg32 > int64(len(s.call.Args)-s.FirstArg) { return fmt.Errorf("format has invalid argument index [%s]", s.Format[start:s.nbytes]) } + s.nbytes++ // skip ']' arg := int(arg32) arg += s.FirstArg - 1 // We want to zero-index the actual arguments. @@ -327,7 +328,7 @@ func (s *FormatDirective) parseSize(kind sizeType) { // Absorb it. s.indexPending = false size := &DirectiveSize{ - Kind: IndexedStar, + Kind: IndexedAsterisk, Size: -1, Range: posRange{ Start: s.indexPos, @@ -347,9 +348,9 @@ func (s *FormatDirective) parseSize(kind sizeType) { panic(kind) } } else { - // Non-indexed star: "%*d". + // Non-indexed asterisk: "%*d". size := &DirectiveSize{ - Kind: Star, + Kind: Asterisk, Size: -1, Range: posRange{ Start: s.nbytes - 1, @@ -398,7 +399,7 @@ func (s *FormatDirective) parseSize(kind sizeType) { } // parsePrecision checks if there's a precision specified after a '.' character. -// If found, it may also parse an index or a star. Returns an error if any index +// If found, it may also parse an index or an asterisk. Returns an error if any index // parsing fails. func (s *FormatDirective) parsePrecision() error { // If there's a period, there may be a precision.