snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s
This commit is contained in:
213
tools/mattermost-govet/mlogFieldNaming/mlogFieldNaming.go
Normal file
213
tools/mattermost-govet/mlogFieldNaming/mlogFieldNaming.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlogFieldNaming
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
var Analyzer = &analysis.Analyzer{
|
||||
Name: "mlogFieldNaming",
|
||||
Doc: "check that mlog field keys are snake_case",
|
||||
Run: run,
|
||||
}
|
||||
|
||||
const mlogPkgPath = "github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
|
||||
// keyedFieldConstructors are the mlog field constructors that take a field key
|
||||
// as their first argument.
|
||||
var keyedFieldConstructors = map[string]bool{
|
||||
"Any": true,
|
||||
"Array": true,
|
||||
"Bool": true,
|
||||
"Duration": true,
|
||||
"Float": true,
|
||||
"Int": true,
|
||||
"Map": true,
|
||||
"Millis": true,
|
||||
"NamedErr": true,
|
||||
"String": true,
|
||||
"Stringer": true,
|
||||
"Time": true,
|
||||
"Uint": true,
|
||||
}
|
||||
|
||||
var snakeCase = regexp.MustCompile(`^[a-z][a-z0-9]*(_[a-z0-9]+)*$`)
|
||||
|
||||
func run(pass *analysis.Pass) (interface{}, error) {
|
||||
for _, file := range pass.Files {
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
call, ok := node.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
fun, ok := calleeSelector(call.Fun)
|
||||
if !ok || !keyedFieldConstructors[fun.Sel.Name] {
|
||||
return true
|
||||
}
|
||||
|
||||
if !isMlogPackage(pass, fun.X) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(call.Args) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
arg := call.Args[0]
|
||||
|
||||
key, ok := constantString(pass, arg)
|
||||
if !ok {
|
||||
// The key isn't statically known, so there's nothing to check.
|
||||
return true
|
||||
}
|
||||
|
||||
if snakeCase.MatchString(key) {
|
||||
return true
|
||||
}
|
||||
|
||||
pass.Report(diagnostic(arg, key))
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func diagnostic(arg ast.Expr, key string) analysis.Diagnostic {
|
||||
// A fix can only rewrite the key in place when it is spelled out as a
|
||||
// string literal. Keys that come from a named constant have to be renamed
|
||||
// at the declaration instead, which is beyond what this analyzer offers.
|
||||
lit, isLiteral := arg.(*ast.BasicLit)
|
||||
isLiteral = isLiteral && lit.Kind == token.STRING
|
||||
|
||||
fixed, canFix := toSnakeCase(key)
|
||||
if !isLiteral || !canFix {
|
||||
return analysis.Diagnostic{
|
||||
Pos: arg.Pos(),
|
||||
End: arg.End(),
|
||||
Message: fmt.Sprintf("mlog field key %q is not snake_case", key),
|
||||
}
|
||||
}
|
||||
|
||||
return analysis.Diagnostic{
|
||||
Pos: arg.Pos(),
|
||||
End: arg.End(),
|
||||
Message: fmt.Sprintf("mlog field key %q is not snake_case, use %q", key, fixed),
|
||||
SuggestedFixes: []analysis.SuggestedFix{{
|
||||
Message: fmt.Sprintf("Rename mlog field key to %q", fixed),
|
||||
TextEdits: []analysis.TextEdit{{
|
||||
Pos: arg.Pos(),
|
||||
End: arg.End(),
|
||||
NewText: []byte(strconv.Quote(fixed)),
|
||||
}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// toSnakeCase converts a field key to snake_case, reporting whether the result
|
||||
// is a valid snake_case key. Keys that cannot be converted mechanically (an
|
||||
// empty key, or one that would start with a digit) are left to the author.
|
||||
func toSnakeCase(key string) (string, bool) {
|
||||
runes := []rune(key)
|
||||
|
||||
var b strings.Builder
|
||||
for i, r := range runes {
|
||||
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
||||
b.WriteRune('_')
|
||||
continue
|
||||
}
|
||||
|
||||
// Break before an uppercase rune that starts a new word, either
|
||||
// following a lowercase rune or a digit ("userId" -> "user_id"), or
|
||||
// ending a run of uppercase runes ("requestURLPath" ->
|
||||
// "request_url_path").
|
||||
if i > 0 && unicode.IsUpper(r) {
|
||||
prev := runes[i-1]
|
||||
startsWord := unicode.IsLower(prev) || unicode.IsDigit(prev)
|
||||
endsAcronym := unicode.IsUpper(prev) && startsNewWord(runes, i+1)
|
||||
if startsWord || endsAcronym {
|
||||
b.WriteRune('_')
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
|
||||
fixed := strings.Trim(b.String(), "_")
|
||||
for strings.Contains(fixed, "__") {
|
||||
fixed = strings.ReplaceAll(fixed, "__", "_")
|
||||
}
|
||||
|
||||
return fixed, snakeCase.MatchString(fixed)
|
||||
}
|
||||
|
||||
// startsNewWord reports whether the lowercase run beginning at i is a new word
|
||||
// rather than a plural suffix on the acronym that precedes it, so that
|
||||
// "requestURLPath" breaks before "Path" but "userIDs" keeps its trailing "s"
|
||||
// ("user_ids", not "user_i_ds").
|
||||
func startsNewWord(runes []rune, i int) bool {
|
||||
if i >= len(runes) || !unicode.IsLower(runes[i]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// A lone "s" pluralizes the acronym instead of starting a word.
|
||||
if runes[i] == 's' && (i+1 == len(runes) || !unicode.IsLower(runes[i+1])) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// calleeSelector returns the pkg.Name selector a call expression resolves to.
|
||||
// Most of the keyed constructors are generic, so an explicitly instantiated
|
||||
// call such as mlog.Int[int64](...) reaches here as an index expression
|
||||
// wrapping the selector rather than as the selector itself.
|
||||
func calleeSelector(fun ast.Expr) (*ast.SelectorExpr, bool) {
|
||||
switch expr := ast.Unparen(fun).(type) {
|
||||
case *ast.IndexExpr:
|
||||
fun = expr.X
|
||||
case *ast.IndexListExpr:
|
||||
fun = expr.X
|
||||
}
|
||||
|
||||
sel, ok := ast.Unparen(fun).(*ast.SelectorExpr)
|
||||
|
||||
return sel, ok
|
||||
}
|
||||
|
||||
func isMlogPackage(pass *analysis.Pass, expr ast.Expr) bool {
|
||||
ident, ok := expr.(*ast.Ident)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
pkgName, ok := pass.TypesInfo.Uses[ident].(*types.PkgName)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return pkgName.Imported().Path() == mlogPkgPath
|
||||
}
|
||||
|
||||
func constantString(pass *analysis.Pass, expr ast.Expr) (string, bool) {
|
||||
tv, ok := pass.TypesInfo.Types[expr]
|
||||
if !ok || tv.Value == nil || tv.Value.Kind() != constant.String {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return constant.StringVal(tv.Value), true
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlogFieldNaming
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
)
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), Analyzer, "a")
|
||||
}
|
||||
|
||||
// TestFixtureCoversEveryConstructor keeps the fixture in step with
|
||||
// keyedFieldConstructors, so that dropping a constructor cannot silently go
|
||||
// unnoticed just because nothing exercises it.
|
||||
func TestFixtureCoversEveryConstructor(t *testing.T) {
|
||||
src, err := os.ReadFile(filepath.Join("testdata", "src", "a", "a.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading fixture: %v", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(src), "\n")
|
||||
for name := range keyedFieldConstructors {
|
||||
covered := false
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "mlog."+name+`("`) && strings.Contains(line, "// want") {
|
||||
covered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !covered {
|
||||
t.Errorf("no diagnostic case for mlog.%s in testdata/src/a/a.go; add one so removing %q from keyedFieldConstructors fails a test", name, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToSnakeCase(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
key string
|
||||
want string
|
||||
wantOK bool
|
||||
}{
|
||||
{"user_id", "user_id", true},
|
||||
{"userId", "user_id", true},
|
||||
{"UserId", "user_id", true},
|
||||
{"userID", "user_id", true},
|
||||
{"ClusterDiscoveryID", "cluster_discovery_id", true},
|
||||
{"requestURL", "request_url", true},
|
||||
{"requestURLPath", "request_url_path", true},
|
||||
{"userIDs", "user_ids", true},
|
||||
{"channelIDs", "channel_ids", true},
|
||||
{"IDs", "ids", true},
|
||||
{"userIDsFoo", "user_ids_foo", true},
|
||||
{"userIDs.foo", "user_ids_foo", true},
|
||||
{"Diff", "diff", true},
|
||||
{"RecvAt", "recv_at", true},
|
||||
{"sharedChannelRemoteId", "shared_channel_remote_id", true},
|
||||
{"channel name", "channel_name", true},
|
||||
{"Possibilities searched", "possibilities_searched", true},
|
||||
{"features.users", "features_users", true},
|
||||
{"sm.Post.ChannelId", "sm_post_channel_id", true},
|
||||
{"user-id", "user_id", true},
|
||||
{"_user_id", "user_id", true},
|
||||
{"user__id", "user_id", true},
|
||||
{"sha256", "sha256", true},
|
||||
{"channelId2", "channel_id2", true},
|
||||
{"2fa", "", false},
|
||||
{"", "", false},
|
||||
{"___", "", false},
|
||||
} {
|
||||
t.Run(tc.key, func(t *testing.T) {
|
||||
got, ok := toSnakeCase(tc.key)
|
||||
if ok != tc.wantOK {
|
||||
t.Fatalf("toSnakeCase(%q) ok = %v, want %v", tc.key, ok, tc.wantOK)
|
||||
}
|
||||
if ok && got != tc.want {
|
||||
t.Fatalf("toSnakeCase(%q) = %q, want %q", tc.key, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
117
tools/mattermost-govet/mlogFieldNaming/testdata/src/a/a.go
vendored
Normal file
117
tools/mattermost-govet/mlogFieldNaming/testdata/src/a/a.go
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
package a
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
snakeKey = "batch_start_time"
|
||||
camelKey = "batchStartTime"
|
||||
)
|
||||
|
||||
type notMlog struct{}
|
||||
|
||||
func (notMlog) String(key string, val string) string { return key }
|
||||
|
||||
func valid() {
|
||||
mlog.Debug("message",
|
||||
mlog.String("user_id", "abc"),
|
||||
mlog.Int("count", 1),
|
||||
mlog.Uint("size", uint(1)),
|
||||
mlog.Float("ratio", 1.0),
|
||||
mlog.Bool("is_leader", true),
|
||||
mlog.Any("data", nil),
|
||||
mlog.Array("ids", []string(nil)),
|
||||
mlog.Map("props", map[string]string(nil)),
|
||||
mlog.Duration("elapsed", nil),
|
||||
mlog.Millis("started_at", 0),
|
||||
mlog.Time("created_at", time.Time{}),
|
||||
mlog.Stringer("channel_type", nil),
|
||||
mlog.NamedErr("upload_err", nil),
|
||||
mlog.Err(errors.New("boom")),
|
||||
)
|
||||
|
||||
// Digits are allowed within and as whole segments.
|
||||
mlog.Info("message", mlog.String("sha256", ""), mlog.String("channel_id_2", ""))
|
||||
|
||||
// Keys resolved from constants are checked too.
|
||||
mlog.Info("message", mlog.String(snakeKey, ""))
|
||||
}
|
||||
|
||||
func fixable() {
|
||||
mlog.Debug("message",
|
||||
mlog.String("userId", ""), // want `mlog field key "userId" is not snake_case, use "user_id"`
|
||||
mlog.String("UserID", ""), // want `mlog field key "UserID" is not snake_case, use "user_id"`
|
||||
mlog.Int("perPage", 0), // want `mlog field key "perPage" is not snake_case, use "per_page"`
|
||||
mlog.String("ClusterDiscoveryID", ""), // want `mlog field key "ClusterDiscoveryID" is not snake_case, use "cluster_discovery_id"`
|
||||
mlog.String("requestURLPath", ""), // want `mlog field key "requestURLPath" is not snake_case, use "request_url_path"`
|
||||
mlog.String("Diff", ""), // want `mlog field key "Diff" is not snake_case, use "diff"`
|
||||
mlog.String("channel name", ""), // want `mlog field key "channel name" is not snake_case, use "channel_name"`
|
||||
mlog.String("features.users", ""), // want `mlog field key "features.users" is not snake_case, use "features_users"`
|
||||
mlog.String("user-id", ""), // want `mlog field key "user-id" is not snake_case, use "user_id"`
|
||||
mlog.String("_user_id", ""), // want `mlog field key "_user_id" is not snake_case, use "user_id"`
|
||||
mlog.String("user__id", ""), // want `mlog field key "user__id" is not snake_case, use "user_id"`
|
||||
mlog.String("channelId2", ""), // want `mlog field key "channelId2" is not snake_case, use "channel_id2"`
|
||||
mlog.Array("userIDs", []string(nil)), // want `mlog field key "userIDs" is not snake_case, use "user_ids"`
|
||||
)
|
||||
}
|
||||
|
||||
// instantiated covers the generic constructors called with explicit type
|
||||
// arguments, where the call expression wraps the selector in an index rather
|
||||
// than being the selector itself.
|
||||
func instantiated() {
|
||||
mlog.Debug("message",
|
||||
mlog.Int[int64]("count", 1),
|
||||
mlog.Array[[]string, string]("ids", nil),
|
||||
mlog.Map[map[string]string, string, string]("props", nil),
|
||||
|
||||
mlog.Int[int64]("perPage", 1), // want `mlog field key "perPage" is not snake_case, use "per_page"`
|
||||
mlog.Array[[]string, string]("userIDs", nil), // want `mlog field key "userIDs" is not snake_case, use "user_ids"`
|
||||
mlog.Map[map[string]string, string, string]("mapProps", nil), // want `mlog field key "mapProps" is not snake_case, use "map_props"`
|
||||
mlog.String[string](camelKey, ""), // want `mlog field key "batchStartTime" is not snake_case$`
|
||||
)
|
||||
}
|
||||
|
||||
func unfixable() {
|
||||
mlog.Debug("message",
|
||||
// Would start with a digit, so there is no mechanical rename.
|
||||
mlog.String("2fa", ""), // want `mlog field key "2fa" is not snake_case$`
|
||||
mlog.String("", ""), // want `mlog field key "" is not snake_case$`
|
||||
)
|
||||
|
||||
// Renaming a constant's value is out of scope, so this is reported without
|
||||
// a suggested fix.
|
||||
mlog.Info("message", mlog.String(camelKey, "")) // want `mlog field key "batchStartTime" is not snake_case$`
|
||||
}
|
||||
|
||||
func ignored(key string) {
|
||||
// Not the mlog package.
|
||||
var n notMlog
|
||||
_ = n.String("userId", "")
|
||||
|
||||
// Not statically known.
|
||||
mlog.Info("message", mlog.String(key, ""))
|
||||
}
|
||||
|
||||
// everyConstructor gives each keyed constructor its own diagnostic, so that
|
||||
// dropping one from keyedFieldConstructors fails a test.
|
||||
func everyConstructor() {
|
||||
mlog.Debug("message",
|
||||
mlog.Any("anyValue", nil), // want `mlog field key "anyValue" is not snake_case, use "any_value"`
|
||||
mlog.Array("arrayIds", []string(nil)), // want `mlog field key "arrayIds" is not snake_case, use "array_ids"`
|
||||
mlog.Bool("boolFlag", true), // want `mlog field key "boolFlag" is not snake_case, use "bool_flag"`
|
||||
mlog.Duration("durationElapsed", nil), // want `mlog field key "durationElapsed" is not snake_case, use "duration_elapsed"`
|
||||
mlog.Float("floatRatio", 1.0), // want `mlog field key "floatRatio" is not snake_case, use "float_ratio"`
|
||||
mlog.Int("intCount", 1), // want `mlog field key "intCount" is not snake_case, use "int_count"`
|
||||
mlog.Map("mapProps", map[string]string(nil)), // want `mlog field key "mapProps" is not snake_case, use "map_props"`
|
||||
mlog.Millis("millisStartedAt", 0), // want `mlog field key "millisStartedAt" is not snake_case, use "millis_started_at"`
|
||||
mlog.NamedErr("namedErr", nil), // want `mlog field key "namedErr" is not snake_case, use "named_err"`
|
||||
mlog.String("stringName", ""), // want `mlog field key "stringName" is not snake_case, use "string_name"`
|
||||
mlog.Stringer("stringerType", nil), // want `mlog field key "stringerType" is not snake_case, use "stringer_type"`
|
||||
mlog.Time("timeCreatedAt", time.Time{}), // want `mlog field key "timeCreatedAt" is not snake_case, use "time_created_at"`
|
||||
mlog.Uint("uintSize", uint(1)), // want `mlog field key "uintSize" is not snake_case, use "uint_size"`
|
||||
)
|
||||
}
|
||||
117
tools/mattermost-govet/mlogFieldNaming/testdata/src/a/a.go.golden
vendored
Normal file
117
tools/mattermost-govet/mlogFieldNaming/testdata/src/a/a.go.golden
vendored
Normal file
@@ -0,0 +1,117 @@
|
||||
package a
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
snakeKey = "batch_start_time"
|
||||
camelKey = "batchStartTime"
|
||||
)
|
||||
|
||||
type notMlog struct{}
|
||||
|
||||
func (notMlog) String(key string, val string) string { return key }
|
||||
|
||||
func valid() {
|
||||
mlog.Debug("message",
|
||||
mlog.String("user_id", "abc"),
|
||||
mlog.Int("count", 1),
|
||||
mlog.Uint("size", uint(1)),
|
||||
mlog.Float("ratio", 1.0),
|
||||
mlog.Bool("is_leader", true),
|
||||
mlog.Any("data", nil),
|
||||
mlog.Array("ids", []string(nil)),
|
||||
mlog.Map("props", map[string]string(nil)),
|
||||
mlog.Duration("elapsed", nil),
|
||||
mlog.Millis("started_at", 0),
|
||||
mlog.Time("created_at", time.Time{}),
|
||||
mlog.Stringer("channel_type", nil),
|
||||
mlog.NamedErr("upload_err", nil),
|
||||
mlog.Err(errors.New("boom")),
|
||||
)
|
||||
|
||||
// Digits are allowed within and as whole segments.
|
||||
mlog.Info("message", mlog.String("sha256", ""), mlog.String("channel_id_2", ""))
|
||||
|
||||
// Keys resolved from constants are checked too.
|
||||
mlog.Info("message", mlog.String(snakeKey, ""))
|
||||
}
|
||||
|
||||
func fixable() {
|
||||
mlog.Debug("message",
|
||||
mlog.String("user_id", ""), // want `mlog field key "userId" is not snake_case, use "user_id"`
|
||||
mlog.String("user_id", ""), // want `mlog field key "UserID" is not snake_case, use "user_id"`
|
||||
mlog.Int("per_page", 0), // want `mlog field key "perPage" is not snake_case, use "per_page"`
|
||||
mlog.String("cluster_discovery_id", ""), // want `mlog field key "ClusterDiscoveryID" is not snake_case, use "cluster_discovery_id"`
|
||||
mlog.String("request_url_path", ""), // want `mlog field key "requestURLPath" is not snake_case, use "request_url_path"`
|
||||
mlog.String("diff", ""), // want `mlog field key "Diff" is not snake_case, use "diff"`
|
||||
mlog.String("channel_name", ""), // want `mlog field key "channel name" is not snake_case, use "channel_name"`
|
||||
mlog.String("features_users", ""), // want `mlog field key "features.users" is not snake_case, use "features_users"`
|
||||
mlog.String("user_id", ""), // want `mlog field key "user-id" is not snake_case, use "user_id"`
|
||||
mlog.String("user_id", ""), // want `mlog field key "_user_id" is not snake_case, use "user_id"`
|
||||
mlog.String("user_id", ""), // want `mlog field key "user__id" is not snake_case, use "user_id"`
|
||||
mlog.String("channel_id2", ""), // want `mlog field key "channelId2" is not snake_case, use "channel_id2"`
|
||||
mlog.Array("user_ids", []string(nil)), // want `mlog field key "userIDs" is not snake_case, use "user_ids"`
|
||||
)
|
||||
}
|
||||
|
||||
// instantiated covers the generic constructors called with explicit type
|
||||
// arguments, where the call expression wraps the selector in an index rather
|
||||
// than being the selector itself.
|
||||
func instantiated() {
|
||||
mlog.Debug("message",
|
||||
mlog.Int[int64]("count", 1),
|
||||
mlog.Array[[]string, string]("ids", nil),
|
||||
mlog.Map[map[string]string, string, string]("props", nil),
|
||||
|
||||
mlog.Int[int64]("per_page", 1), // want `mlog field key "perPage" is not snake_case, use "per_page"`
|
||||
mlog.Array[[]string, string]("user_ids", nil), // want `mlog field key "userIDs" is not snake_case, use "user_ids"`
|
||||
mlog.Map[map[string]string, string, string]("map_props", nil), // want `mlog field key "mapProps" is not snake_case, use "map_props"`
|
||||
mlog.String[string](camelKey, ""), // want `mlog field key "batchStartTime" is not snake_case$`
|
||||
)
|
||||
}
|
||||
|
||||
func unfixable() {
|
||||
mlog.Debug("message",
|
||||
// Would start with a digit, so there is no mechanical rename.
|
||||
mlog.String("2fa", ""), // want `mlog field key "2fa" is not snake_case$`
|
||||
mlog.String("", ""), // want `mlog field key "" is not snake_case$`
|
||||
)
|
||||
|
||||
// Renaming a constant's value is out of scope, so this is reported without
|
||||
// a suggested fix.
|
||||
mlog.Info("message", mlog.String(camelKey, "")) // want `mlog field key "batchStartTime" is not snake_case$`
|
||||
}
|
||||
|
||||
func ignored(key string) {
|
||||
// Not the mlog package.
|
||||
var n notMlog
|
||||
_ = n.String("userId", "")
|
||||
|
||||
// Not statically known.
|
||||
mlog.Info("message", mlog.String(key, ""))
|
||||
}
|
||||
|
||||
// everyConstructor gives each keyed constructor its own diagnostic, so that
|
||||
// dropping one from keyedFieldConstructors fails a test.
|
||||
func everyConstructor() {
|
||||
mlog.Debug("message",
|
||||
mlog.Any("any_value", nil), // want `mlog field key "anyValue" is not snake_case, use "any_value"`
|
||||
mlog.Array("array_ids", []string(nil)), // want `mlog field key "arrayIds" is not snake_case, use "array_ids"`
|
||||
mlog.Bool("bool_flag", true), // want `mlog field key "boolFlag" is not snake_case, use "bool_flag"`
|
||||
mlog.Duration("duration_elapsed", nil), // want `mlog field key "durationElapsed" is not snake_case, use "duration_elapsed"`
|
||||
mlog.Float("float_ratio", 1.0), // want `mlog field key "floatRatio" is not snake_case, use "float_ratio"`
|
||||
mlog.Int("int_count", 1), // want `mlog field key "intCount" is not snake_case, use "int_count"`
|
||||
mlog.Map("map_props", map[string]string(nil)), // want `mlog field key "mapProps" is not snake_case, use "map_props"`
|
||||
mlog.Millis("millis_started_at", 0), // want `mlog field key "millisStartedAt" is not snake_case, use "millis_started_at"`
|
||||
mlog.NamedErr("named_err", nil), // want `mlog field key "namedErr" is not snake_case, use "named_err"`
|
||||
mlog.String("string_name", ""), // want `mlog field key "stringName" is not snake_case, use "string_name"`
|
||||
mlog.Stringer("stringer_type", nil), // want `mlog field key "stringerType" is not snake_case, use "stringer_type"`
|
||||
mlog.Time("time_created_at", time.Time{}), // want `mlog field key "timeCreatedAt" is not snake_case, use "time_created_at"`
|
||||
mlog.Uint("uint_size", uint(1)), // want `mlog field key "uintSize" is not snake_case, use "uint_size"`
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package mlog
|
||||
|
||||
type Field struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func Any(key string, val any) Field { return Field{Key: key} }
|
||||
func Duration(key string, val any) Field { return Field{Key: key} }
|
||||
func Millis(key string, val int64) Field { return Field{Key: key} }
|
||||
func NamedErr(key string, err error) Field { return Field{Key: key} }
|
||||
func Stringer(key string, val any) Field { return Field{Key: key} }
|
||||
func Time(key string, val any) Field { return Field{Key: key} }
|
||||
|
||||
// The remaining constructors are generic, mirroring the real mlog package, so
|
||||
// that the fixture exercises explicitly instantiated calls.
|
||||
|
||||
func Array[S ~[]E, E any](key string, val S) Field { return Field{Key: key} }
|
||||
|
||||
func Bool[T ~bool](key string, val T) Field { return Field{Key: key} }
|
||||
|
||||
func Float[T ~float32 | ~float64](key string, val T) Field { return Field{Key: key} }
|
||||
|
||||
func Int[T ~int | ~int8 | ~int16 | ~int32 | ~int64](key string, val T) Field {
|
||||
return Field{Key: key}
|
||||
}
|
||||
|
||||
func Map[M ~map[K]V, K comparable, V any](key string, val M) Field { return Field{Key: key} }
|
||||
|
||||
func String[T ~string | ~[]byte](key string, val T) Field { return Field{Key: key} }
|
||||
|
||||
func Uint[T ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr](key string, val T) Field {
|
||||
return Field{Key: key}
|
||||
}
|
||||
|
||||
func Err(err error) Field { return Field{Key: "error"} }
|
||||
|
||||
func Debug(msg string, fields ...Field) {}
|
||||
func Info(msg string, fields ...Field) {}
|
||||
func Error(msg string, fields ...Field) {}
|
||||
Reference in New Issue
Block a user