├── .gitattributes ├── .github └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── cmd └── go-printf-func-name │ └── main.go ├── go.mod ├── go.sum ├── pkg └── analyzer │ ├── analyzer.go │ └── analyzer_test.go └── testdata └── src └── p └── p.go /.gitattributes: -------------------------------------------------------------------------------- 1 | go.sum linguist-generated 2 | * text=auto eol=lf 3 | *.ps1 text eol=crlf 4 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | env: 15 | GOLANGCI_LINT_VERSION: v2.0 16 | CGO_ENABLED: 0 17 | 18 | jobs: 19 | golangci: 20 | strategy: 21 | matrix: 22 | go: [stable] 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | name: lint 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-go@v5 29 | with: 30 | go-version: ${{ matrix.go }} 31 | 32 | - name: Check and get dependencies 33 | run: | 34 | go mod download 35 | go mod tidy 36 | git diff --exit-code go.mod 37 | git diff --exit-code go.sum 38 | 39 | - name: golangci-lint 40 | uses: golangci/golangci-lint-action@v7 41 | with: 42 | version: ${{ env.GOLANGCI_LINT_VERSION }} 43 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | env: 15 | CGO_ENABLED: 0 16 | 17 | jobs: 18 | cross: 19 | name: Go 20 | strategy: 21 | matrix: 22 | go-version: [ oldstable, stable ] 23 | os: [ubuntu-latest, macos-latest, windows-latest] 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-go@v5 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | 31 | - name: Test 32 | run: make test 33 | 34 | - name: Build 35 | run: make build 36 | 37 | - name: Vet integration 38 | run: make vet 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /go-printf-func-name 3 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | formatters: 4 | enable: 5 | - gci 6 | - gofumpt 7 | 8 | linters: 9 | default: none 10 | enable: 11 | - asasalint 12 | - asciicheck 13 | - bidichk 14 | - containedctx 15 | - contextcheck 16 | - copyloopvar 17 | - cyclop 18 | - dogsled 19 | - dupl 20 | - dupword 21 | - durationcheck 22 | - err113 23 | - errcheck 24 | - errname 25 | - errorlint 26 | - fatcontext 27 | - forbidigo 28 | - funlen 29 | - gocheckcompilerdirectives 30 | - gochecknoglobals 31 | - gochecknoinits 32 | - gocognit 33 | - goconst 34 | - gocritic 35 | - gocyclo 36 | - godot 37 | - godox 38 | - gomoddirectives 39 | - gomodguard 40 | - goprintffuncname 41 | - gosec 42 | - govet 43 | - importas 44 | - inamedparam 45 | - ineffassign 46 | - interfacebloat 47 | - intrange 48 | - ireturn 49 | - loggercheck 50 | - maintidx 51 | - makezero 52 | - mirror 53 | - misspell 54 | - musttag 55 | - nestif 56 | - nilerr 57 | - nlreturn 58 | - noctx 59 | - nolintlint 60 | - nonamedreturns 61 | - perfsprint 62 | - predeclared 63 | - reassign 64 | - revive 65 | - staticcheck 66 | - tagalign 67 | - tagliatelle 68 | - testableexamples 69 | - testifylint 70 | - thelper 71 | - tparallel 72 | - unconvert 73 | - unparam 74 | - unused 75 | - usestdlibvars 76 | - wastedassign 77 | - whitespace 78 | - wrapcheck 79 | - wsl 80 | 81 | settings: 82 | cyclop: 83 | max-complexity: 15 84 | staticcheck: 85 | checks: 86 | - '*' 87 | - -ST1000 88 | exclusions: 89 | warn-unused: true 90 | presets: 91 | - comments 92 | rules: 93 | - linters: 94 | - gochecknoglobals 95 | path: pkg/analyzer/analyzer.go 96 | text: Analyzer is a global variable 97 | 98 | issues: 99 | max-issues-per-linter: 0 100 | max-same-issues: 0 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Golangci-lint authors 4 | Copyright (c) 2020 Isaev Denis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test build vet 2 | 3 | default: lint test vet 4 | 5 | test: 6 | go test -v -cover ./... 7 | 8 | lint: 9 | golangci-lint run 10 | 11 | build: 12 | go build ./cmd/go-printf-func-name/ 13 | 14 | vet: build 15 | go vet -vettool=./go-printf-func-name ./... 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-printf-func-name 2 | 3 | The Go linter `go-printf-func-name` checks that printf-like functions are named with `f` at the end. 4 | 5 | ## Example 6 | 7 | `myLog` should be named `myLogf` by Go convention: 8 | 9 | ```go 10 | package main 11 | 12 | import "log" 13 | 14 | func myLog(format string, args ...interface{}) { 15 | const prefix = "[my] " 16 | log.Printf(prefix + format, args...) 17 | } 18 | ``` 19 | 20 | ```console 21 | $ go vet -vettool=$(which go-printf-func-name) ./... 22 | ./main.go:5:1: printf-like formatting function 'myLog' should be named 'myLogf' 23 | ``` 24 | -------------------------------------------------------------------------------- /cmd/go-printf-func-name/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | "github.com/golangci/go-printf-func-name/pkg/analyzer" 7 | "golang.org/x/tools/go/analysis/singlechecker" 8 | ) 9 | 10 | func main() { 11 | // Don't use it: just to not crash on -unsafeptr flag from go vet 12 | flag.Bool("unsafeptr", false, "") 13 | 14 | singlechecker.Main(analyzer.Analyzer) 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/golangci/go-printf-func-name 2 | 3 | go 1.23.0 4 | 5 | require golang.org/x/tools v0.31.0 6 | 7 | require ( 8 | golang.org/x/mod v0.24.0 // indirect 9 | golang.org/x/sync v0.12.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 4 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 5 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 6 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 7 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 8 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 9 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzer.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "go/ast" 5 | "strings" 6 | 7 | "golang.org/x/tools/go/analysis" 8 | "golang.org/x/tools/go/analysis/passes/inspect" 9 | "golang.org/x/tools/go/ast/inspector" 10 | ) 11 | 12 | var Analyzer = &analysis.Analyzer{ 13 | Name: "goprintffuncname", 14 | Doc: "Checks that printf-like functions are named with `f` at the end.", 15 | Run: run, 16 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 17 | } 18 | 19 | func run(pass *analysis.Pass) (interface{}, error) { 20 | insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 21 | 22 | nodeFilter := []ast.Node{ 23 | (*ast.FuncDecl)(nil), 24 | } 25 | 26 | insp.Preorder(nodeFilter, func(node ast.Node) { 27 | funcDecl := node.(*ast.FuncDecl) 28 | 29 | if res := funcDecl.Type.Results; res != nil && len(res.List) != 0 { 30 | return 31 | } 32 | 33 | params := funcDecl.Type.Params.List 34 | if len(params) < 2 { // [0] must be format (string), [1] must be args (...interface{}) 35 | return 36 | } 37 | 38 | formatParamType, ok := params[len(params)-2].Type.(*ast.Ident) 39 | if !ok { // first param type isn't identificator so it can't be of type "string" 40 | return 41 | } 42 | 43 | if formatParamType.Name != "string" { // first param (format) type is not string 44 | return 45 | } 46 | 47 | formatParamNames := params[len(params)-2].Names 48 | if len(formatParamNames) == 0 || formatParamNames[len(formatParamNames)-1].Name != "format" { 49 | return 50 | } 51 | 52 | argsParamType, ok := params[len(params)-1].Type.(*ast.Ellipsis) 53 | if !ok { // args are not ellipsis (...args) 54 | return 55 | } 56 | 57 | elementType, ok := argsParamType.Elt.(*ast.InterfaceType) 58 | if !ok { // args are not of interface type, but we need interface{} 59 | return 60 | } 61 | 62 | if elementType.Methods != nil && len(elementType.Methods.List) != 0 { 63 | return // has >= 1 method in interface, but we need an empty interface "interface{}" 64 | } 65 | 66 | if strings.HasSuffix(funcDecl.Name.Name, "f") { 67 | return 68 | } 69 | 70 | pass.Reportf(node.Pos(), "printf-like formatting function '%s' should be named '%sf'", 71 | funcDecl.Name.Name, funcDecl.Name.Name) 72 | }) 73 | 74 | return nil, nil 75 | } 76 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package analyzer_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/golangci/go-printf-func-name/pkg/analyzer" 9 | "golang.org/x/tools/go/analysis/analysistest" 10 | ) 11 | 12 | func TestAll(t *testing.T) { 13 | wd, err := os.Getwd() 14 | if err != nil { 15 | t.Fatalf("Failed to get wd: %s", err) 16 | } 17 | 18 | testdata := filepath.Join(filepath.Dir(filepath.Dir(wd)), "testdata") 19 | analysistest.Run(t, testdata, analyzer.Analyzer, "p") 20 | } 21 | -------------------------------------------------------------------------------- /testdata/src/p/p.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | func notPrintfFuncAtAll() {} 4 | 5 | func funcWithEllipsis(args ...interface{}) {} 6 | 7 | func printfLikeButWithStrings(format string, args ...string) {} 8 | 9 | func printfLikeButWithBadFormat(format int, args ...interface{}) {} 10 | 11 | func secondArgIsNotEllipsis(format string, arg int) {} 12 | 13 | func printfLikeButWithExtraInterfaceMethods(format string, args ...interface { 14 | String() string 15 | }) { 16 | } 17 | 18 | func prinfLikeFuncf(format string, args ...interface{}) {} 19 | 20 | func prinfLikeFuncWithReturnValue(format string, args ...interface{}) string { 21 | return "" 22 | } 23 | 24 | func prinfLikeFuncWithAnotherFormatArgName(msg string, args ...interface{}) {} 25 | 26 | func prinfLikeFunc(format string, args ...interface{}) {} // want "printf-like formatting function" 27 | 28 | func prinfLikeFuncWithExtraArgs1(extraArg, format string, args ...interface{}) {} // want "printf-like formatting function" 29 | 30 | func prinfLikeFuncWithExtraArgs2(extraArg int, format string, args ...interface{}) {} // want "printf-like formatting function" 31 | --------------------------------------------------------------------------------