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

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View 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
}

View File

@@ -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)
}
})
}
}

View 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"`
)
}

View 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"`
)
}

View File

@@ -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) {}