diff options
| author | Taras Madan <tarasmadan@google.com> | 2023-02-22 22:16:50 +0100 |
|---|---|---|
| committer | Taras Madan <tarasmadan@google.com> | 2023-02-24 12:47:23 +0100 |
| commit | 4165372ec8fd142475a4e35fd0cf4f8042132208 (patch) | |
| tree | 21cd62211b4dd80bee469054c5b65db77342333c /vendor/github.com/timonwong | |
| parent | 2b3ed821a493b8936c8bacfa6f8b4f1c90a00855 (diff) | |
dependencies: update
set go min requirements to 1.19
update dependencies
update vendor
Diffstat (limited to 'vendor/github.com/timonwong')
19 files changed, 1386 insertions, 0 deletions
diff --git a/vendor/github.com/timonwong/loggercheck/.codecov.yml b/vendor/github.com/timonwong/loggercheck/.codecov.yml new file mode 100644 index 000000000..ef90457ca --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/.codecov.yml @@ -0,0 +1,16 @@ +coverage: + range: 70..90 # green if 90+, red if 70- + status: + patch: + # coverage status for pull request diff + default: + threshold: 1% # allow a little drop + project: + # coverage status for whole project + default: + target: auto # use coverage of base commit as target + threshold: 1% # allow a little drop + +ignore: + - "plugin/**" + - "cmd/**" diff --git a/vendor/github.com/timonwong/loggercheck/.gitignore b/vendor/github.com/timonwong/loggercheck/.gitignore new file mode 100644 index 000000000..33df0df91 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/.gitignore @@ -0,0 +1,9 @@ +.vscode/ +.idea/ + +bin/ +vendor/ + +dist/ + +cover.out diff --git a/vendor/github.com/timonwong/loggercheck/.golangci.yml b/vendor/github.com/timonwong/loggercheck/.golangci.yml new file mode 100644 index 000000000..287327893 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/.golangci.yml @@ -0,0 +1,94 @@ +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 2 + min-occurrences: 3 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + - whyNoLint + gocyclo: + min-complexity: 15 + goimports: + local-prefixes: github.com/timonwong/loggercheck + gomnd: + # don't include the "operation" and "assign" + checks: + - argument + - case + - condition + - return + ignored-numbers: + - '0' + - '1' + - '2' + - '3' + ignored-functions: + - strings.SplitN + - strconv.ParseInt + govet: + check-shadowing: true + lll: + line-length: 140 + misspell: + locale: US + nolintlint: + allow-unused: false # report any unused nolint directives + require-explanation: false # don't require an explanation for nolint directives + require-specific: false # don't require nolint directives to be specific about which linter is being skipped +linters: + disable-all: true + enable: + - bodyclose + - dogsled + - dupl + - errcheck + - exportloopref + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofumpt + - goimports + - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - noctx + - nolintlint + - revive + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - whitespace + +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - gomnd + +run: + timeout: 5m + go: '1.17' + skip-dirs: + - testdata
\ No newline at end of file diff --git a/vendor/github.com/timonwong/loggercheck/.goreleaser.yml b/vendor/github.com/timonwong/loggercheck/.goreleaser.yml new file mode 100644 index 000000000..55ffe7ea0 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/.goreleaser.yml @@ -0,0 +1,59 @@ +--- +project_name: loggercheck + +release: + github: + owner: timonwong + name: loggercheck + +builds: + - binary: loggercheck + goos: + - darwin + - windows + - linux + goarch: + - '386' + - amd64 + - arm + - arm64 + goarm: + - '7' + env: + - CGO_ENABLED=0 + ignore: + - goos: darwin + goarch: '386' + main: ./cmd/loggercheck/ + flags: + - -trimpath + ldflags: -s -w + +archives: + - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + format_overrides: + - goos: windows + format: zip + files: + - LICENSE + - README.md + +snapshot: + name_template: '{{ incpatch .Version }}-next' + +checksum: + name_template: 'checksums.txt' + +changelog: + sort: asc + filters: + exclude: + - '(?i)^docs?:' + - '(?i)^docs\([^:]+\):' + - '(?i)^docs\[[^:]+\]:' + - '^tests?:' + - '(?i)^dev:' + - '^build\(deps\): bump .* in /docs \(#\d+\)' + - '^build\(deps\): bump .* in /\.github/peril \(#\d+\)' + - Merge pull request + - Merge branch diff --git a/vendor/github.com/timonwong/loggercheck/LICENSE b/vendor/github.com/timonwong/loggercheck/LICENSE new file mode 100644 index 000000000..fb6531090 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Timon Wong + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/timonwong/loggercheck/Makefile b/vendor/github.com/timonwong/loggercheck/Makefile new file mode 100644 index 000000000..37bf87202 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/Makefile @@ -0,0 +1,22 @@ +.PHONY: lint +lint: + golangci-lint run ./... + +.PHONY: test-deps +test-deps: + cd testdata/src/a && go mod vendor + +.PHONY: test +test: test-deps + go test -v -covermode=atomic -coverprofile=cover.out -coverpkg ./... ./... + +.PHONY: build +build: + go build -o bin/loggercheck ./cmd/loggercheck + +.PHONY: build-plugin +build-plugin: + CGO_ENABLED=1 go build -o bin/loggercheck.so -buildmode=plugin ./plugin + +.PHONY: build-all +build-all: build build-plugin diff --git a/vendor/github.com/timonwong/loggercheck/README.md b/vendor/github.com/timonwong/loggercheck/README.md new file mode 100644 index 000000000..14aeca371 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/README.md @@ -0,0 +1,103 @@ +# loggercheck + +## Description + +A linter checks the odd number of key and value pairs for common logger libraries: +- [kitlog](https://github.com/go-kit/log) +- [klog](https://github.com/kubernetes/klog) +- [logr](https://github.com/go-logr/logr) +- [zap](https://github.com/uber-go/zap) + +## Badges + + +[](https://app.codecov.io/gh/timonwong/loggercheck) +[](/LICENSE) +[](https://github.com/timonwong/loggercheck/releases/latest) + +## Install + +```shel +go install github.com/timonwong/loggercheck/cmd/loggercheck +``` + +## Usage + +``` +loggercheck: Checks key value pairs for common logger libraries (kitlog,logr,klog,zap). + +Usage: loggercheck [-flag] [package] + + +Flags: + -V print version and exit + -all + no effect (deprecated) + -c int + display offending line with this many lines of context (default -1) + -cpuprofile string + write CPU profile to this file + -debug string + debug flags, any subset of "fpstv" + -disable value + comma-separated list of disabled logger checker (kitlog,klog,logr,zap) (default kitlog) + -fix + apply all suggested fixes + -flags + print analyzer flags in JSON + -json + emit JSON output + -memprofile string + write memory profile to this file + -noprintflike + require printf-like format specifier not present in args + -requirestringkey + require all logging keys to be inlined constant strings + -rulefile string + path to a file contains a list of rules + -source + no effect (deprecated) + -tags string + no effect (deprecated) + -test + indicates whether test files should be analyzed, too (default true) + -trace string + write trace log to this file + -v no effect (deprecated) +``` + +## Example + +```go +package a + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" +) + +func Example() { + log := logr.Discard() + log = log.WithValues("key") + log.Info("message", "key1", "value1", "key2", "value2", "key3") + log.Error(fmt.Errorf("error"), "message", "key1", "value1", "key2") + log.Error(fmt.Errorf("error"), "message", "key1", "value1", "key2", "value2") + + var log2 logr.Logger + log2 = log + log2.Info("message", "key1") + + log3 := logr.FromContextOrDiscard(context.TODO()) + log3.Error(fmt.Errorf("error"), "message", "key1") +} +``` + +``` +a.go:12:23: odd number of arguments passed as key-value pairs for logging +a.go:13:22: odd number of arguments passed as key-value pairs for logging +a.go:14:44: odd number of arguments passed as key-value pairs for logging +a.go:19:23: odd number of arguments passed as key-value pairs for logging +a.go:22:45: odd number of arguments passed as key-value pairs for logging +```
\ No newline at end of file diff --git a/vendor/github.com/timonwong/loggercheck/internal/bytebufferpool/pool.go b/vendor/github.com/timonwong/loggercheck/internal/bytebufferpool/pool.go new file mode 100644 index 000000000..9d88d21c4 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/bytebufferpool/pool.go @@ -0,0 +1,22 @@ +package bytebufferpool + +import ( + "bytes" + "sync" +) + +var pool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func Get() *bytes.Buffer { + buf := pool.Get().(*bytes.Buffer) + buf.Reset() + return buf +} + +func Put(buf *bytes.Buffer) { + pool.Put(buf) +} diff --git a/vendor/github.com/timonwong/loggercheck/internal/checkers/checker.go b/vendor/github.com/timonwong/loggercheck/internal/checkers/checker.go new file mode 100644 index 000000000..5615636ef --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/checkers/checker.go @@ -0,0 +1,59 @@ +package checkers + +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" +) + +type Config struct { + RequireStringKey bool + NoPrintfLike bool +} + +type CallContext struct { + Expr *ast.CallExpr + Func *types.Func + Signature *types.Signature +} + +type Checker interface { + FilterKeyAndValues(pass *analysis.Pass, keyAndValues []ast.Expr) []ast.Expr + CheckLoggingKey(pass *analysis.Pass, keyAndValues []ast.Expr) + CheckPrintfLikeSpecifier(pass *analysis.Pass, args []ast.Expr) +} + +func ExecuteChecker(c Checker, pass *analysis.Pass, call CallContext, cfg Config) { + params := call.Signature.Params() + nparams := params.Len() // variadic => nonzero + startIndex := nparams - 1 + + lastArg := params.At(nparams - 1) + iface, ok := lastArg.Type().(*types.Slice).Elem().(*types.Interface) + if !ok || !iface.Empty() { + return // final (args) param is not ...interface{} + } + + keyValuesArgs := c.FilterKeyAndValues(pass, call.Expr.Args[startIndex:]) + + if len(keyValuesArgs)%2 != 0 { + firstArg := keyValuesArgs[0] + lastArg := keyValuesArgs[len(keyValuesArgs)-1] + pass.Report(analysis.Diagnostic{ + Pos: firstArg.Pos(), + End: lastArg.End(), + Category: DiagnosticCategory, + Message: "odd number of arguments passed as key-value pairs for logging", + }) + } + + if cfg.RequireStringKey { + c.CheckLoggingKey(pass, keyValuesArgs) + } + + if cfg.NoPrintfLike { + // Check all args + c.CheckPrintfLikeSpecifier(pass, call.Expr.Args) + } +} diff --git a/vendor/github.com/timonwong/loggercheck/internal/checkers/common.go b/vendor/github.com/timonwong/loggercheck/internal/checkers/common.go new file mode 100644 index 000000000..42cbd0193 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/checkers/common.go @@ -0,0 +1,49 @@ +package checkers + +import ( + "go/ast" + "go/constant" + "go/printer" + "go/token" + "go/types" + "unicode/utf8" + + "golang.org/x/tools/go/analysis" + + "github.com/timonwong/loggercheck/internal/bytebufferpool" +) + +const ( + DiagnosticCategory = "logging" +) + +// extractValueFromStringArg returns true if the argument is a string type (literal or constant). +func extractValueFromStringArg(pass *analysis.Pass, arg ast.Expr) (value string, ok bool) { + if typeAndValue, ok := pass.TypesInfo.Types[arg]; ok { + if typ, ok := typeAndValue.Type.(*types.Basic); ok && typ.Kind() == types.String && typeAndValue.Value != nil { + return constant.StringVal(typeAndValue.Value), true + } + } + + return "", false +} + +func renderNodeEllipsis(fset *token.FileSet, v interface{}) string { + const maxLen = 20 + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + _ = printer.Fprint(buf, fset, v) + s := buf.String() + if utf8.RuneCountInString(s) > maxLen { + // Copied from go/constant/value.go + i := 0 + for n := 0; n < maxLen-3; n++ { + _, size := utf8.DecodeRuneInString(s[i:]) + i += size + } + s = s[:i] + "..." + } + return s +} diff --git a/vendor/github.com/timonwong/loggercheck/internal/checkers/general.go b/vendor/github.com/timonwong/loggercheck/internal/checkers/general.go new file mode 100644 index 000000000..6512cce30 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/checkers/general.go @@ -0,0 +1,68 @@ +package checkers + +import ( + "fmt" + "go/ast" + + "golang.org/x/tools/go/analysis" + + "github.com/timonwong/loggercheck/internal/checkers/printf" + "github.com/timonwong/loggercheck/internal/stringutil" +) + +type General struct{} + +func (g General) FilterKeyAndValues(_ *analysis.Pass, keyAndValues []ast.Expr) []ast.Expr { + return keyAndValues +} + +func (g General) CheckLoggingKey(pass *analysis.Pass, keyAndValues []ast.Expr) { + for i := 0; i < len(keyAndValues); i += 2 { + arg := keyAndValues[i] + if value, ok := extractValueFromStringArg(pass, arg); ok { + if stringutil.IsASCII(value) { + continue + } + + pass.Report(analysis.Diagnostic{ + Pos: arg.Pos(), + End: arg.End(), + Category: DiagnosticCategory, + Message: fmt.Sprintf( + "logging keys are expected to be alphanumeric strings, please remove any non-latin characters from %q", + value), + }) + } else { + pass.Report(analysis.Diagnostic{ + Pos: arg.Pos(), + End: arg.End(), + Category: DiagnosticCategory, + Message: fmt.Sprintf( + "logging keys are expected to be inlined constant strings, please replace %q provided with string", + renderNodeEllipsis(pass.Fset, arg)), + }) + } + } +} + +func (g General) CheckPrintfLikeSpecifier(pass *analysis.Pass, args []ast.Expr) { + for _, arg := range args { + format, ok := extractValueFromStringArg(pass, arg) + if !ok { + continue + } + + if specifier, ok := printf.IsPrintfLike(format); ok { + pass.Report(analysis.Diagnostic{ + Pos: arg.Pos(), + End: arg.End(), + Category: DiagnosticCategory, + Message: fmt.Sprintf("logging message should not use format specifier %q", specifier), + }) + + return // One error diagnostic is enough + } + } +} + +var _ Checker = (*General)(nil) diff --git a/vendor/github.com/timonwong/loggercheck/internal/checkers/printf/printf.go b/vendor/github.com/timonwong/loggercheck/internal/checkers/printf/printf.go new file mode 100644 index 000000000..b38f46f20 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/checkers/printf/printf.go @@ -0,0 +1,252 @@ +package printf + +import ( + "strconv" + "strings" + "unicode/utf8" +) + +// Copied from golang.org/x/tools +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +type printVerb struct { + verb rune // User may provide verb through Formatter; could be a rune. + flags string // known flags are all ASCII +} + +// Common flag sets for printf verbs. +const ( + noFlag = "" + numFlag = " -+.0" + sharpNumFlag = " -+.0#" + allFlags = " -+.0#" +) + +// printVerbs identifies which flags are known to printf for each verb. +var printVerbs = []printVerb{ + // '-' is a width modifier, always valid. + // '.' is a precision for float, max width for strings. + // '+' is required sign for numbers, Go format for %v. + // '#' is alternate format for several verbs. + // ' ' is spacer for numbers + {'%', noFlag}, + {'b', sharpNumFlag}, + {'c', "-"}, + {'d', numFlag}, + {'e', sharpNumFlag}, + {'E', sharpNumFlag}, + {'f', sharpNumFlag}, + {'F', sharpNumFlag}, + {'g', sharpNumFlag}, + {'G', sharpNumFlag}, + {'o', sharpNumFlag}, + {'O', sharpNumFlag}, + {'p', "-#"}, + {'q', " -+.0#"}, + {'s', " -+.0"}, + {'t', "-"}, + {'T', "-"}, + {'U', "-#"}, + {'v', allFlags}, + {'w', allFlags}, + {'x', sharpNumFlag}, + {'X', sharpNumFlag}, +} + +// formatState holds the parsed representation of a printf directive such as "%3.*[4]d". +// It is constructed by parsePrintfVerb. +type formatState struct { + verb rune // the format verb: 'd' for "%d" + format string // the full format directive from % through verb, "%.3d". + flags []byte // the list of # + etc. + // Used only during parse. + hasIndex bool // Whether the argument is indexed. + indexPending bool // Whether we have an indexed argument that has not resolved. + nbytes int // number of bytes of the format string consumed. +} + +// parseFlags accepts any printf flags. +func (s *formatState) parseFlags() { + for s.nbytes < len(s.format) { + switch c := s.format[s.nbytes]; c { + case '#', '0', '+', '-', ' ': + s.flags = append(s.flags, c) + s.nbytes++ + default: + return + } + } +} + +// scanNum advances through a decimal number if present. +func (s *formatState) scanNum() { + for ; s.nbytes < len(s.format); s.nbytes++ { + c := s.format[s.nbytes] + if c < '0' || '9' < c { + return + } + } +} + +func stringIndexAt(s, substr string, start int) int { + idx := strings.Index(s[start:], substr) + if idx < 0 { + return idx + } + return idx + start +} + +// parseIndex scans an index expression. It returns false if there is a syntax error. +func (s *formatState) parseIndex() bool { + if s.nbytes == len(s.format) || s.format[s.nbytes] != '[' { + return true + } + // Argument index present. + s.nbytes++ // skip '[' + start := s.nbytes + s.scanNum() + ok := true + if s.nbytes == len(s.format) || s.nbytes == start || s.format[s.nbytes] != ']' { + ok = false + s.nbytes = stringIndexAt(s.format, "]", start) + if s.nbytes < 0 { + return false + } + } + arg32, err := strconv.ParseInt(s.format[start:s.nbytes], 10, 32) + if err != nil || !ok || arg32 <= 0 { + return false + } + s.nbytes++ // skip ']' + s.hasIndex = true + s.indexPending = true + return true +} + +// parseNum scans a width or precision (or *). +func (s *formatState) parseNum() { + if s.nbytes < len(s.format) && s.format[s.nbytes] == '*' { + if s.indexPending { // Absorb it. + s.indexPending = false + } + s.nbytes++ + } else { + s.scanNum() + } +} + +// parsePrecision scans for a precision. It returns false if there's a bad index expression. +func (s *formatState) parsePrecision() bool { + // If there's a period, there may be a precision. + if s.nbytes < len(s.format) && s.format[s.nbytes] == '.' { + s.flags = append(s.flags, '.') // Treat precision as a flag. + s.nbytes++ + if !s.parseIndex() { + return false + } + s.parseNum() + } + return true +} + +// parsePrintfVerb looks the formatting directive that begins the format string +// and returns a formatState that encodes what the directive wants, without looking +// at the actual arguments present in the call. The result is nil if there is an error. +func parsePrintfVerb(format string) *formatState { + state := &formatState{ + format: format, + flags: make([]byte, 0, 5), //nolint:gomnd + nbytes: 1, // There's guaranteed to be a percent sign. + } + + // There may be flags. + state.parseFlags() + // There may be an index. + if !state.parseIndex() { + return nil + } + // There may be a width. + state.parseNum() + // There may be a precision. + if !state.parsePrecision() { + return nil + } + // Now a verb, possibly prefixed by an index (which we may already have). + if !state.indexPending && !state.parseIndex() { + return nil + } + if state.nbytes == len(state.format) { + // missing verb at end of string + return nil + } + verb, w := utf8.DecodeRuneInString(state.format[state.nbytes:]) + state.verb = verb + state.nbytes += w + state.format = state.format[:state.nbytes] + return state +} + +func containsAll(s string, pattern []byte) bool { + for _, c := range pattern { + if !strings.ContainsRune(s, rune(c)) { + return false + } + } + return true +} + +func isPrintfArg(state *formatState) bool { + var v printVerb + found := false + // Linear scan is fast enough for a small list. + for _, v = range printVerbs { + if v.verb == state.verb { + found = true + break + } + } + + if !found { + // unknown verb, just skip + return false + } + + if !containsAll(v.flags, state.flags) { + // unrecognized format flag, just skip + return false + } + + return true +} + +func IsPrintfLike(format string) (firstSpecifier string, ok bool) { + if !strings.Contains(format, "%") { + return "", false + } + + for i, w := 0, 0; i < len(format); i += w { + w = 1 + if format[i] != '%' { + continue + } + + state := parsePrintfVerb(format[i:]) + if state == nil { + return "", false + } + + w = len(state.format) + if !isPrintfArg(state) { + return "", false + } + + if !ok { + firstSpecifier = state.format + ok = true + } + } + + return +} diff --git a/vendor/github.com/timonwong/loggercheck/internal/checkers/zap.go b/vendor/github.com/timonwong/loggercheck/internal/checkers/zap.go new file mode 100644 index 000000000..2356f8348 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/checkers/zap.go @@ -0,0 +1,42 @@ +package checkers + +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" +) + +type Zap struct { + General +} + +func (z Zap) FilterKeyAndValues(pass *analysis.Pass, keyAndValues []ast.Expr) []ast.Expr { + // Check the argument count + filtered := make([]ast.Expr, 0, len(keyAndValues)) + for _, arg := range keyAndValues { + // Skip any zapcore.Field we found + switch arg := arg.(type) { + case *ast.CallExpr, *ast.Ident: + typ := pass.TypesInfo.TypeOf(arg) + switch typ := typ.(type) { + case *types.Named: + obj := typ.Obj() + // This is a strongly-typed field. Consume it and move on. + // Actually it's go.uber.org/zap/zapcore.Field, however for simplicity + // we don't check the import path + if obj != nil && obj.Name() == "Field" { + continue + } + default: + // pass + } + } + + filtered = append(filtered, arg) + } + + return filtered +} + +var _ Checker = (*Zap)(nil) diff --git a/vendor/github.com/timonwong/loggercheck/internal/rules/rules.go b/vendor/github.com/timonwong/loggercheck/internal/rules/rules.go new file mode 100644 index 000000000..27d6ebb27 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/rules/rules.go @@ -0,0 +1,201 @@ +package rules + +import ( + "bufio" + "errors" + "fmt" + "go/types" + "io" + "strings" + + "github.com/timonwong/loggercheck/internal/bytebufferpool" +) + +var ErrInvalidRule = errors.New("invalid rule format") + +const CustomRulesetName = "custom" + +type Ruleset struct { + Name string + PackageImport string + Rules []FuncRule + + ruleIndicesByFuncName map[string][]int +} + +func (rs *Ruleset) Match(fn *types.Func) bool { + // PackageImport is already checked (by indices), skip checking it here + sig := fn.Type().(*types.Signature) // it's safe since we already checked + + // Fail fast if the function name is not in the rule list. + indices, ok := rs.ruleIndicesByFuncName[fn.Name()] + if !ok { + return false + } + + for _, idx := range indices { + rule := &rs.Rules[idx] + if matchRule(rule, sig) { + return true + } + } + + return false +} + +func receiverTypeOf(recvType types.Type) string { + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + var recvNamed *types.Named + switch recvType := recvType.(type) { + case *types.Pointer: + buf.WriteByte('*') + if elem, ok := recvType.Elem().(*types.Named); ok { + recvNamed = elem + } + case *types.Named: + recvNamed = recvType + } + + if recvNamed == nil { + // not supported type + return "" + } + + buf.WriteString(recvNamed.Obj().Name()) + typeParams := recvNamed.TypeParams() + if typeParamsLen := typeParams.Len(); typeParamsLen > 0 { + buf.WriteByte('[') + for i := 0; i < typeParamsLen; i++ { + if i > 0 { + // comma as separator + buf.WriteByte(',') + } + p := typeParams.At(i) + buf.WriteString(p.Obj().Name()) + } + buf.WriteByte(']') + } + + return buf.String() +} + +func matchRule(p *FuncRule, sig *types.Signature) bool { + // we do not check package import here since it's already checked in Match() + recv := sig.Recv() + isReceiver := recv != nil + if isReceiver != p.IsReceiver { + return false + } + + if isReceiver { + recvType := recv.Type() + receiverType := receiverTypeOf(recvType) + if receiverType != p.ReceiverType { + return false + } + } + + return true +} + +type FuncRule struct { // package import should be accessed from Rulset + ReceiverType string + FuncName string + IsReceiver bool +} + +func ParseFuncRule(rule string) (packageImport string, pat FuncRule, err error) { + lastDot := strings.LastIndexFunc(rule, func(r rune) bool { + return r == '.' || r == '/' + }) + if lastDot == -1 || rule[lastDot] == '/' { + return "", pat, ErrInvalidRule + } + + importOrReceiver := rule[:lastDot] + pat.FuncName = rule[lastDot+1:] + + if strings.HasPrefix(rule, "(") { // package + if !strings.HasSuffix(importOrReceiver, ")") { + return "", FuncRule{}, ErrInvalidRule + } + + var isPointerReceiver bool + pat.IsReceiver = true + receiver := importOrReceiver[1 : len(importOrReceiver)-1] + if strings.HasPrefix(receiver, "*") { + isPointerReceiver = true + receiver = receiver[1:] + } + + typeDotIdx := strings.LastIndexFunc(receiver, func(r rune) bool { + return r == '.' || r == '/' + }) + if typeDotIdx == -1 || receiver[typeDotIdx] == '/' { + return "", FuncRule{}, ErrInvalidRule + } + receiverType := receiver[typeDotIdx+1:] + if isPointerReceiver { + receiverType = "*" + receiverType + } + pat.ReceiverType = receiverType + packageImport = receiver[:typeDotIdx] + } else { + packageImport = importOrReceiver + } + + return packageImport, pat, nil +} + +func ParseRules(lines []string) (result []Ruleset, err error) { + rulesByImport := make(map[string][]FuncRule) + for i, line := range lines { + if line == "" { + continue + } + + if strings.HasPrefix(line, "#") { // comments + continue + } + + packageImport, pat, err := ParseFuncRule(line) + if err != nil { + return nil, fmt.Errorf("error parse rule at line %d: %w", i+1, err) + } + rulesByImport[packageImport] = append(rulesByImport[packageImport], pat) + } + + for packageImport, rules := range rulesByImport { + ruleIndicesByFuncName := make(map[string][]int, len(rules)) + for idx, rule := range rules { + fnName := rule.FuncName + ruleIndicesByFuncName[fnName] = append(ruleIndicesByFuncName[fnName], idx) + } + + result = append(result, Ruleset{ + Name: CustomRulesetName, // NOTE(timonwong) Always "custom" for custom rule + PackageImport: packageImport, + Rules: rules, + ruleIndicesByFuncName: ruleIndicesByFuncName, + }) + } + return result, nil +} + +func ParseRuleFile(r io.Reader) (result []Ruleset, err error) { + // Rule files are relatively small, so read it into string slice first. + var lines []string + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + lines = append(lines, line) + } + if err := scanner.Err(); err != nil { + return nil, err + } + + return ParseRules(lines) +} diff --git a/vendor/github.com/timonwong/loggercheck/internal/sets/string.go b/vendor/github.com/timonwong/loggercheck/internal/sets/string.go new file mode 100644 index 000000000..daf8d57fc --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/sets/string.go @@ -0,0 +1,59 @@ +package sets + +import ( + "sort" + "strings" +) + +type Empty struct{} + +type StringSet map[string]Empty + +func NewString(items ...string) StringSet { + s := make(StringSet) + s.Insert(items...) + return s +} + +func (s StringSet) Insert(items ...string) { + for _, item := range items { + s[item] = Empty{} + } +} + +func (s StringSet) Has(item string) bool { + _, contained := s[item] + return contained +} + +func (s StringSet) List() []string { + if len(s) == 0 { + return nil + } + + res := make([]string, 0, len(s)) + for key := range s { + res = append(res, key) + } + sort.Strings(res) + return res +} + +// Set implements flag.Value interface. +func (s *StringSet) Set(v string) error { + v = strings.TrimSpace(v) + if v == "" { + *s = nil + return nil + } + + parts := strings.Split(v, ",") + set := NewString(parts...) + *s = set + return nil +} + +// String implements flag.Value interface +func (s StringSet) String() string { + return strings.Join(s.List(), ",") +} diff --git a/vendor/github.com/timonwong/loggercheck/internal/stringutil/is.go b/vendor/github.com/timonwong/loggercheck/internal/stringutil/is.go new file mode 100644 index 000000000..a36b742fc --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/internal/stringutil/is.go @@ -0,0 +1,15 @@ +package stringutil + +import "unicode/utf8" + +// IsASCII returns true if string are ASCII. +func IsASCII(s string) bool { + for _, r := range s { + if r >= utf8.RuneSelf { + // Not ASCII. + return false + } + } + + return true +} diff --git a/vendor/github.com/timonwong/loggercheck/loggercheck.go b/vendor/github.com/timonwong/loggercheck/loggercheck.go new file mode 100644 index 000000000..8bd10aee8 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/loggercheck.go @@ -0,0 +1,196 @@ +package loggercheck + +import ( + "flag" + "fmt" + "go/ast" + "go/types" + "os" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + + "github.com/timonwong/loggercheck/internal/checkers" + "github.com/timonwong/loggercheck/internal/rules" + "github.com/timonwong/loggercheck/internal/sets" +) + +const Doc = `Checks key value pairs for common logger libraries (kitlog,klog,logr,zap).` + +func NewAnalyzer(opts ...Option) *analysis.Analyzer { + l := newLoggerCheck(opts...) + a := &analysis.Analyzer{ + Name: "loggercheck", + Doc: Doc, + Flags: *l.fs, + Run: l.run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + } + return a +} + +type loggercheck struct { + fs *flag.FlagSet + + disable sets.StringSet // flag -disable + ruleFile string // flag -rulefile + requireStringKey bool // flag -requirestringkey + noPrintfLike bool // flag -noprintflike + + rules []string // used for external integration, for example golangci-lint + rulesetList []rules.Ruleset // populate at runtime + rulesetIndicesByImport map[string][]int // ruleset index, populate at runtime +} + +func newLoggerCheck(opts ...Option) *loggercheck { + fs := flag.NewFlagSet("loggercheck", flag.ExitOnError) + l := &loggercheck{ + fs: fs, + disable: sets.NewString("kitlog"), + rulesetList: append([]rules.Ruleset{}, staticRuleList...), // ensure we make a clone of static rules first + } + + fs.StringVar(&l.ruleFile, "rulefile", "", "path to a file contains a list of rules") + fs.Var(&l.disable, "disable", "comma-separated list of disabled logger checker (kitlog,klog,logr,zap)") + fs.BoolVar(&l.requireStringKey, "requirestringkey", false, "require all logging keys to be inlined constant strings") + fs.BoolVar(&l.noPrintfLike, "noprintflike", false, "require printf-like format specifier not present in args") + + for _, opt := range opts { + opt(l) + } + + return l +} + +func (l *loggercheck) isCheckerDisabled(name string) bool { + return l.disable.Has(name) +} + +// vendorLessPath returns the devendorized version of the import path ipath. +// For example: "a/vendor/github.com/go-logr/logr" will become "github.com/go-logr/logr". +func vendorLessPath(ipath string) string { + if i := strings.LastIndex(ipath, "/vendor/"); i >= 0 { + return ipath[i+len("/vendor/"):] + } + return ipath +} + +func (l *loggercheck) getCheckerForFunc(fn *types.Func) checkers.Checker { + pkg := fn.Pkg() + if pkg == nil { + return nil + } + + pkgPath := vendorLessPath(pkg.Path()) + indices := l.rulesetIndicesByImport[pkgPath] + + for _, idx := range indices { + rs := &l.rulesetList[idx] + if l.isCheckerDisabled(rs.Name) { + // Skip ignored logger checker. + continue + } + + if !rs.Match(fn) { + continue + } + + checker := checkerByRulesetName[rs.Name] + if checker == nil { + return checkers.General{} + } + return checker + } + + return nil +} + +func (l *loggercheck) checkLoggerArguments(pass *analysis.Pass, call *ast.CallExpr) { + fn, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func) + if fn == nil { + return // function pointer is not supported + } + + sig, ok := fn.Type().(*types.Signature) + if !ok || !sig.Variadic() { + return // not variadic + } + + // ellipsis args is hard, just skip + if call.Ellipsis.IsValid() { + return + } + + checker := l.getCheckerForFunc(fn) + if checker == nil { + return + } + + checkers.ExecuteChecker(checker, pass, checkers.CallContext{ + Expr: call, + Func: fn, + Signature: sig, + }, checkers.Config{ + RequireStringKey: l.requireStringKey, + NoPrintfLike: l.noPrintfLike, + }) +} + +func (l *loggercheck) processConfig() error { + if l.ruleFile != "" { // flags takes precedence over configs + f, err := os.Open(l.ruleFile) + if err != nil { + return fmt.Errorf("failed to open rule file: %w", err) + } + defer f.Close() + + custom, err := rules.ParseRuleFile(f) + if err != nil { + return fmt.Errorf("failed to parse rule file: %w", err) + } + l.rulesetList = append(l.rulesetList, custom...) + } else if len(l.rules) > 0 { + custom, err := rules.ParseRules(l.rules) + if err != nil { + return fmt.Errorf("failed to parse rules: %w", err) + } + l.rulesetList = append(l.rulesetList, custom...) + } + + // Build index + indices := make(map[string][]int) + for i, rs := range l.rulesetList { + indices[rs.PackageImport] = append(indices[rs.PackageImport], i) + } + l.rulesetIndicesByImport = indices + + return nil +} + +func (l *loggercheck) run(pass *analysis.Pass) (interface{}, error) { + err := l.processConfig() + if err != nil { + return nil, err + } + + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + insp.Preorder(nodeFilter, func(node ast.Node) { + call := node.(*ast.CallExpr) + + typ := pass.TypesInfo.Types[call.Fun].Type + if typ == nil { + // Skip checking functions with unknown type. + return + } + + l.checkLoggerArguments(pass, call) + }) + + return nil, nil +} diff --git a/vendor/github.com/timonwong/loggercheck/options.go b/vendor/github.com/timonwong/loggercheck/options.go new file mode 100644 index 000000000..6b5f00af1 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/options.go @@ -0,0 +1,31 @@ +package loggercheck + +import ( + "github.com/timonwong/loggercheck/internal/sets" +) + +type Option func(*loggercheck) + +func WithDisable(disable []string) Option { + return func(l *loggercheck) { + l.disable = sets.NewString(disable...) + } +} + +func WithRules(customRules []string) Option { + return func(l *loggercheck) { + l.rules = customRules + } +} + +func WithRequireStringKey(requireStringKey bool) Option { + return func(l *loggercheck) { + l.requireStringKey = requireStringKey + } +} + +func WithNoPrintfLike(noPrintfLike bool) Option { + return func(l *loggercheck) { + l.noPrintfLike = noPrintfLike + } +} diff --git a/vendor/github.com/timonwong/loggercheck/staticrules.go b/vendor/github.com/timonwong/loggercheck/staticrules.go new file mode 100644 index 000000000..f955b3434 --- /dev/null +++ b/vendor/github.com/timonwong/loggercheck/staticrules.go @@ -0,0 +1,68 @@ +package loggercheck + +import ( + "errors" + "fmt" + + "github.com/timonwong/loggercheck/internal/checkers" + "github.com/timonwong/loggercheck/internal/rules" +) + +var ( + staticRuleList = []rules.Ruleset{ + mustNewStaticRuleSet("logr", []string{ + "(github.com/go-logr/logr.Logger).Error", + "(github.com/go-logr/logr.Logger).Info", + "(github.com/go-logr/logr.Logger).WithValues", + }), + mustNewStaticRuleSet("klog", []string{ + "k8s.io/klog/v2.InfoS", + "k8s.io/klog/v2.InfoSDepth", + "k8s.io/klog/v2.ErrorS", + "(k8s.io/klog/v2.Verbose).InfoS", + "(k8s.io/klog/v2.Verbose).InfoSDepth", + "(k8s.io/klog/v2.Verbose).ErrorS", + }), + mustNewStaticRuleSet("zap", []string{ + "(*go.uber.org/zap.SugaredLogger).With", + "(*go.uber.org/zap.SugaredLogger).Debugw", + "(*go.uber.org/zap.SugaredLogger).Infow", + "(*go.uber.org/zap.SugaredLogger).Warnw", + "(*go.uber.org/zap.SugaredLogger).Errorw", + "(*go.uber.org/zap.SugaredLogger).DPanicw", + "(*go.uber.org/zap.SugaredLogger).Panicw", + "(*go.uber.org/zap.SugaredLogger).Fatalw", + }), + mustNewStaticRuleSet("kitlog", []string{ + "github.com/go-kit/log.With", + "github.com/go-kit/log.WithPrefix", + "github.com/go-kit/log.WithSuffix", + "(github.com/go-kit/log.Logger).Log", + }), + } + checkerByRulesetName = map[string]checkers.Checker{ + // by default, checkers.General will be used. + "zap": checkers.Zap{}, + } +) + +// mustNewStaticRuleSet only called at init, catch errors during development. +// In production it will not panic. +func mustNewStaticRuleSet(name string, lines []string) rules.Ruleset { + if len(lines) == 0 { + panic(errors.New("no rules provided")) + } + + rulesetList, err := rules.ParseRules(lines) + if err != nil { + panic(err) + } + + if len(rulesetList) != 1 { + panic(fmt.Errorf("expected 1 ruleset, got %d", len(rulesetList))) + } + + ruleset := rulesetList[0] + ruleset.Name = name + return ruleset +} |
