diff --git a/README.md b/README.md index 7d7d972..6b7026a 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,4 @@ Ensure consistent code-style when using `log/slog`. * Forbid mixing key-value pairs and attributes in a single function call (default) * Enforce using either key-value pairs or attributes for the entire project (optional) * Enforce using constants (or custom `slog.Attr` constructors) instead of raw keys (optional) -* [WIP] Enforce putting arguments on separate lines (optional) +* Enforce putting arguments on separate lines (optional) diff --git a/sloglint.go b/sloglint.go index b32edcf..53b9084 100644 --- a/sloglint.go +++ b/sloglint.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "go/ast" + "go/token" "go/types" "strconv" @@ -15,9 +16,10 @@ import ( ) type Options struct { - KVOnly bool - AttrOnly bool - NoRawKeys bool + KVOnly bool + AttrOnly bool + NoRawKeys bool + ArgsOnSepLines bool } // New creates a new sloglint analyzer. @@ -54,6 +56,7 @@ func flags(opts *Options) flag.FlagSet { boolVar(&opts.KVOnly, "kv-only", "enforce using key-value pairs only (incompatible with -attr-only)") boolVar(&opts.AttrOnly, "attr-only", "enforce using attributes only (incompatible with -kv-only)") boolVar(&opts.NoRawKeys, "no-raw-keys", "forbid using raw keys") + boolVar(&opts.ArgsOnSepLines, "args-on-sep-lines", "enforce putting arguments on separate lines") return *fs } @@ -125,13 +128,16 @@ func run(pass *analysis.Pass, opts *Options) { pass.Reportf(call.Pos(), "key-value pairs and attributes should not be mixed") } - if opts.NoRawKeys && rawKeysUsed(keys, attrs, pass.TypesInfo) { + if opts.NoRawKeys && rawKeysUsed(pass.TypesInfo, keys, attrs) { pass.Reportf(call.Pos(), "raw keys should not be used") } + if opts.ArgsOnSepLines && argsOnSameLine(pass.Fset, call, keys, attrs) { + pass.Reportf(call.Pos(), "arguments should be put on separate lines") + } }) } -func rawKeysUsed(keys, attrs []ast.Expr, info *types.Info) bool { +func rawKeysUsed(info *types.Info, keys, attrs []ast.Expr) bool { isConst := func(expr ast.Expr) bool { ident, ok := expr.(*ast.Ident) return ok && ident.Obj != nil && ident.Obj.Kind == ast.Con @@ -190,3 +196,26 @@ func rawKeysUsed(keys, attrs []ast.Expr, info *types.Info) bool { return false } + +func argsOnSameLine(fset *token.FileSet, call ast.Expr, keys, attrs []ast.Expr) bool { + if len(keys)+len(attrs) <= 1 { + return false // special case: slog.Info("msg", "key", "value") is ok. + } + + l := len(keys) + len(attrs) + 1 + args := make([]ast.Expr, 0, l) + args = append(args, call) + args = append(args, keys...) + args = append(args, attrs...) + + lines := make(map[int]struct{}, l) + for _, arg := range args { + line := fset.Position(arg.Pos()).Line + if _, ok := lines[line]; ok { + return true + } + lines[line] = struct{}{} + } + + return false +} diff --git a/sloglint_test.go b/sloglint_test.go index ca59638..8d9aa20 100644 --- a/sloglint_test.go +++ b/sloglint_test.go @@ -29,4 +29,9 @@ func TestAnalyzer(t *testing.T) { analyzer := sloglint.New(&sloglint.Options{NoRawKeys: true}) analysistest.Run(t, testdata, analyzer, "no_raw_keys") }) + + t.Run("arguments on separate lines", func(t *testing.T) { + analyzer := sloglint.New(&sloglint.Options{ArgsOnSepLines: true}) + analysistest.Run(t, testdata, analyzer, "args_on_sep_lines") + }) } diff --git a/testdata/src/args_on_sep_lines/args_on_sep_lines.go b/testdata/src/args_on_sep_lines/args_on_sep_lines.go new file mode 100644 index 0000000..bf46c93 --- /dev/null +++ b/testdata/src/args_on_sep_lines/args_on_sep_lines.go @@ -0,0 +1,19 @@ +package args_on_sep_lines + +import "log/slog" + +func tests() { + slog.Info("msg", "foo", 1) + slog.Info("msg", + "foo", 1, + "bar", 2, + ) + + slog.Info("msg", "foo", 1, "bar", 2) // want `arguments should be put on separate lines` + slog.Info("msg", "foo", 1, // want `arguments should be put on separate lines` + "bar", 2, + ) + slog.Info("msg", // want `arguments should be put on separate lines` + "foo", 1, "bar", 2, + ) +}