go-tools icon indicating copy to clipboard operation
go-tools copied to clipboard

spurious SA5009 with fmt.Formatter

Open ghost opened this issue 2 years ago • 3 comments

go version go1.20 windows/amd64

using this code:

package main

import "fmt"

type export int

func (e export) Format(f fmt.State, verb rune) {
   if verb == '_' {
      fmt.Fprint(f, "_")
   }
   fmt.Fprint(f, int(e))
}

func main() {
   var e export = 1
   fmt.Printf("%v %_\n", e, e)
}

it runs fine:

> go run exports.go
1 _1

but I get a message:

> staticcheck exports.go
exports.go:16:15: couldn't parse format string (SA5009)

running staticcheck from current master.

ghost avatar Jun 26 '23 00:06 ghost

Our format string parser currently assumes that only the letters a-z and A-Z are valid verbs. Seeing how both fmt and go vet accept your format string, that assumption is probably wrong.

dominikh avatar Jun 26 '23 11:06 dominikh

OK from my testing, these are all valid:

fmt.Printf("%!", export{}); fmt.Println()
fmt.Printf("%$", export{}); fmt.Println()
fmt.Printf("%%%v", export{}); fmt.Println()
fmt.Printf("%&", export{}); fmt.Println()
fmt.Printf("%'", export{}); fmt.Println()
fmt.Printf("%(", export{}); fmt.Println()
fmt.Printf("%)", export{}); fmt.Println()
fmt.Printf("%,", export{}); fmt.Println()
fmt.Printf("%.", export{}); fmt.Println()
fmt.Printf("%/", export{}); fmt.Println()
fmt.Printf("%:", export{}); fmt.Println()
fmt.Printf("%;", export{}); fmt.Println()
fmt.Printf("%<", export{}); fmt.Println()
fmt.Printf("%=", export{}); fmt.Println()
fmt.Printf("%>", export{}); fmt.Println()
fmt.Printf("%?", export{}); fmt.Println()
fmt.Printf("%@", export{}); fmt.Println()
fmt.Printf("%A", export{}); fmt.Println()
fmt.Printf("%B", export{}); fmt.Println()
fmt.Printf("%C", export{}); fmt.Println()
fmt.Printf("%D", export{}); fmt.Println()
fmt.Printf("%E", export{}); fmt.Println()
fmt.Printf("%F", export{}); fmt.Println()
fmt.Printf("%G", export{}); fmt.Println()
fmt.Printf("%H", export{}); fmt.Println()
fmt.Printf("%I", export{}); fmt.Println()
fmt.Printf("%J", export{}); fmt.Println()
fmt.Printf("%K", export{}); fmt.Println()
fmt.Printf("%L", export{}); fmt.Println()
fmt.Printf("%M", export{}); fmt.Println()
fmt.Printf("%N", export{}); fmt.Println()
fmt.Printf("%O", export{}); fmt.Println()
fmt.Printf("%P", export{}); fmt.Println()
fmt.Printf("%Q", export{}); fmt.Println()
fmt.Printf("%R", export{}); fmt.Println()
fmt.Printf("%S", export{}); fmt.Println()
fmt.Printf("%T", export{}); fmt.Println()
fmt.Printf("%U", export{}); fmt.Println()
fmt.Printf("%V", export{}); fmt.Println()
fmt.Printf("%W", export{}); fmt.Println()
fmt.Printf("%X", export{}); fmt.Println()
fmt.Printf("%Y", export{}); fmt.Println()
fmt.Printf("%Z", export{}); fmt.Println()
fmt.Printf("%\"", export{}); fmt.Println()
fmt.Printf("%\\", export{}); fmt.Println()
fmt.Printf("%]", export{}); fmt.Println()
fmt.Printf("%^", export{}); fmt.Println()
fmt.Printf("%_", export{}); fmt.Println()
fmt.Printf("%`", export{}); fmt.Println()
fmt.Printf("%a", export{}); fmt.Println()
fmt.Printf("%b", export{}); fmt.Println()
fmt.Printf("%c", export{}); fmt.Println()
fmt.Printf("%d", export{}); fmt.Println()
fmt.Printf("%e", export{}); fmt.Println()
fmt.Printf("%f", export{}); fmt.Println()
fmt.Printf("%g", export{}); fmt.Println()
fmt.Printf("%h", export{}); fmt.Println()
fmt.Printf("%i", export{}); fmt.Println()
fmt.Printf("%j", export{}); fmt.Println()
fmt.Printf("%k", export{}); fmt.Println()
fmt.Printf("%l", export{}); fmt.Println()
fmt.Printf("%m", export{}); fmt.Println()
fmt.Printf("%n", export{}); fmt.Println()
fmt.Printf("%o", export{}); fmt.Println()
fmt.Printf("%p", export{}); fmt.Println()
fmt.Printf("%q", export{}); fmt.Println()
fmt.Printf("%r", export{}); fmt.Println()
fmt.Printf("%s", export{}); fmt.Println()
fmt.Printf("%t", export{}); fmt.Println()
fmt.Printf("%u", export{}); fmt.Println()
fmt.Printf("%v", export{}); fmt.Println()
fmt.Printf("%w", export{}); fmt.Println()
fmt.Printf("%x", export{}); fmt.Println()
fmt.Printf("%y", export{}); fmt.Println()
fmt.Printf("%z", export{}); fmt.Println()
fmt.Printf("%{", export{}); fmt.Println()
fmt.Printf("%|", export{}); fmt.Println()
fmt.Printf("%}", export{}); fmt.Println()
fmt.Printf("%~", export{}); fmt.Println()

and these gave errors:

// %!(NOVERB)%!(EXTRA main.export=v)
fmt.Printf("%#", export{}); fmt.Println()

// %!(BADWIDTH)%!(NOVERB)
fmt.Printf("%*", export{}); fmt.Println()

// %!(NOVERB)%!(EXTRA main.export=v)
fmt.Printf("%+", export{}); fmt.Println()

// %!(NOVERB)%!(EXTRA main.export=v)
fmt.Printf("%-", export{}); fmt.Println()

// %!(NOVERB)%!(EXTRA main.export=v)
fmt.Printf("%0", export{}); fmt.Println()
fmt.Printf("%1", export{}); fmt.Println()
fmt.Printf("%2", export{}); fmt.Println()
fmt.Printf("%3", export{}); fmt.Println()
fmt.Printf("%4", export{}); fmt.Println()
fmt.Printf("%5", export{}); fmt.Println()
fmt.Printf("%6", export{}); fmt.Println()
fmt.Printf("%7", export{}); fmt.Println()
fmt.Printf("%8", export{}); fmt.Println()
fmt.Printf("%9", export{}); fmt.Println()

// %!(NOVERB)
fmt.Printf("%[", export{}); fmt.Println()

so maybe you could test if %!(NOVERB) is found in the output or something? or just expand the current valid values.

ghost avatar Jun 26 '23 22:06 ghost

You can use any codepoint; see doPrintf() in fmt/print.go. This is valid:

type export int

func (e export) Format(f fmt.State, verb rune) {
    switch verb {
    case '🤷':
            fmt.Fprint(f, 0)
    case '🤑':
            fmt.Fprint(f, int(e)*10)
    case '𓂺':
            fmt.Fprint(f, int(e)*100)
    default:
            fmt.Fprintf(f, "%%(unknown verb '%c')", verb)
    }
}

func main() {
    fmt.Printf("%🤷 %[1]🤑 %[1]𓂺\n", export(42))
}

At the very least it should relax the check only if the argument implements fmt.Formatter(). Although even then, I'm not sure. Realistically, using a non-standard verb is pretty much always an error.

Unfortunately OP is gone so can't ask them to describe their use case.

arp242 avatar May 27 '24 16:05 arp242