Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement -no-global #32

Merged
merged 2 commits into from Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -17,6 +17,7 @@ With `sloglint` you can enforce various rules for `log/slog` based on your prefe

* Enforce not mixing key-value pairs and attributes (default)
* Enforce using either key-value pairs only or attributes only (optional)
* Enforce not using global loggers (optional)
* Enforce using methods that accept a context (optional)
* Enforce using static log messages (optional)
* Enforce using constants instead of raw keys (optional)
Expand Down Expand Up @@ -70,6 +71,17 @@ In contrast, the `attr-only` option causes `sloglint` to report any use of key-v
slog.Info("a user has logged in", "user_id", 42) // sloglint: key-value pairs should not be used
```

### No global

Some projects prefer to pass loggers as explicit dependencies.
The `no-global` option causes `sloglint` to report the usage of global loggers.

```go
slog.Info("a user has logged in", "user_id", 42) // sloglint: global logger should not be used
```

Possible values are `all` (report all global loggers) and `default` (report only the default `slog` logger).

### Context only

Some `slog.Handler` implementations make use of the given `context.Context` (e.g. to access context values).
Expand Down
59 changes: 50 additions & 9 deletions sloglint.go
Expand Up @@ -9,6 +9,7 @@
"go/token"
"go/types"
"strconv"
"strings"

"github.com/ettle/strcase"
"golang.org/x/tools/go/analysis"
Expand All @@ -22,6 +23,7 @@
NoMixedArgs bool // Enforce not mixing key-value pairs and attributes (default true).
KVOnly bool // Enforce using key-value pairs only (overrides NoMixedArgs, incompatible with AttrOnly).
AttrOnly bool // Enforce using attributes only (overrides NoMixedArgs, incompatible with KVOnly).
NoGlobal string // Enforce not using global loggers ("all" or "default").
ContextOnly bool // Enforce using methods that accept a context.
StaticMsg bool // Enforce using static log messages.
NoRawKeys bool // Enforce using constants instead of raw keys.
Expand All @@ -43,11 +45,19 @@
if opts.KVOnly && opts.AttrOnly {
return nil, fmt.Errorf("sloglint: Options.KVOnly and Options.AttrOnly: %w", errIncompatible)
}

switch opts.NoGlobal {
case "", "all", "default":
default:
return nil, fmt.Errorf("sloglint: Options.NoGlobal=%s: %w", opts.NoGlobal, errInvalidValue)
}

switch opts.KeyNamingCase {
case "", snakeCase, kebabCase, camelCase, pascalCase:
default:
return nil, fmt.Errorf("sloglint: Options.KeyNamingCase=%s: %w", opts.KeyNamingCase, errInvalidValue)
}

run(pass, opts)
return nil, nil
},
Expand All @@ -60,30 +70,34 @@
)

func flags(opts *Options) flag.FlagSet {
fs := flag.NewFlagSet("sloglint", flag.ContinueOnError)
fset := flag.NewFlagSet("sloglint", flag.ContinueOnError)

boolVar := func(value *bool, name, usage string) {
fs.Func(name, usage, func(s string) error {
fset.Func(name, usage, func(s string) error {
v, err := strconv.ParseBool(s)
*value = v
return err
})
}

strVar := func(value *string, name, usage string) {
fset.Func(name, usage, func(s string) error {
*value = s
return nil
})

Check warning on line 87 in sloglint.go

View check run for this annotation

Codecov / codecov/patch

sloglint.go#L85-L87

Added lines #L85 - L87 were not covered by tests
}

boolVar(&opts.NoMixedArgs, "no-mixed-args", "enforce not mixing key-value pairs and attributes (default true)")
boolVar(&opts.KVOnly, "kv-only", "enforce using key-value pairs only (overrides -no-mixed-args, incompatible with -attr-only)")
boolVar(&opts.AttrOnly, "attr-only", "enforce using attributes only (overrides -no-mixed-args, incompatible with -kv-only)")
strVar(&opts.NoGlobal, "no-global", "enforce not using global loggers (all|default)")
boolVar(&opts.ContextOnly, "context-only", "enforce using methods that accept a context")
boolVar(&opts.StaticMsg, "static-msg", "enforce using static log messages")
boolVar(&opts.NoRawKeys, "no-raw-keys", "enforce using constants instead of raw keys")
strVar(&opts.KeyNamingCase, "key-naming-case", "enforce a single key naming convention (snake|kebab|camel|pascal)")
boolVar(&opts.ArgsOnSepLines, "args-on-sep-lines", "enforce putting arguments on separate lines")

fs.Func("key-naming-case", "enforce a single key naming convention (snake|kebab|camel|pascal)", func(s string) error {
opts.KeyNamingCase = s
return nil
})

return *fs
return *fset
}

var slogFuncs = map[string]int{ // funcName:argsPos
Expand Down Expand Up @@ -139,17 +153,30 @@
return
}

argsPos, ok := slogFuncs[fn.FullName()]
name := fn.FullName()
argsPos, ok := slogFuncs[name]
if !ok {
return
}

switch opts.NoGlobal {
case "all":
if strings.HasPrefix(name, "log/slog.") || globalLoggerUsed(pass.TypesInfo, call.Fun) {
pass.Reportf(call.Pos(), "global logger should not be used")
}
case "default":
if strings.HasPrefix(name, "log/slog.") {
pass.Reportf(call.Pos(), "default logger should not be used")
}
}

if opts.ContextOnly {
typ := pass.TypesInfo.TypeOf(call.Args[0])
if typ != nil && typ.String() != "context.Context" {
pass.Reportf(call.Pos(), "methods without a context should not be used")
}
}

if opts.StaticMsg && !staticMsg(call.Args[argsPos-1]) {
pass.Reportf(call.Pos(), "message should be a string literal or a constant")
}
Expand Down Expand Up @@ -189,6 +216,7 @@
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")
}
Expand All @@ -206,6 +234,19 @@
})
}

func globalLoggerUsed(info *types.Info, expr ast.Expr) bool {
selector, ok := expr.(*ast.SelectorExpr)
if !ok {
return false
}

Check warning on line 241 in sloglint.go

View check run for this annotation

Codecov / codecov/patch

sloglint.go#L240-L241

Added lines #L240 - L241 were not covered by tests
ident, ok := selector.X.(*ast.Ident)
if !ok {
return false
}

Check warning on line 245 in sloglint.go

View check run for this annotation

Codecov / codecov/patch

sloglint.go#L244-L245

Added lines #L244 - L245 were not covered by tests
obj := info.ObjectOf(ident)
return obj.Parent() == obj.Pkg().Scope()
}

func staticMsg(expr ast.Expr) bool {
switch msg := expr.(type) {
case *ast.BasicLit: // e.g. slog.Info("msg")
Expand Down
8 changes: 6 additions & 2 deletions sloglint_internal_test.go
Expand Up @@ -10,11 +10,15 @@ func TestOptions(t *testing.T) {
opts Options
err error
}{
"incompatible": {
"KVOnly+AttrOnly: incompatible": {
opts: Options{KVOnly: true, AttrOnly: true},
err: errIncompatible,
},
"invalid value": {
"NoGlobal: invalid value": {
opts: Options{NoGlobal: "-"},
err: errInvalidValue,
},
"KeyNamingCase: invalid value": {
opts: Options{KeyNamingCase: "-"},
err: errInvalidValue,
},
Expand Down
10 changes: 10 additions & 0 deletions sloglint_test.go
Expand Up @@ -25,6 +25,16 @@ func TestAnalyzer(t *testing.T) {
analysistest.Run(t, testdata, analyzer, "attr_only")
})

t.Run("no global (all)", func(t *testing.T) {
analyzer := sloglint.New(&sloglint.Options{NoGlobal: "all"})
analysistest.Run(t, testdata, analyzer, "no_global_all")
})

t.Run("no global (default)", func(t *testing.T) {
analyzer := sloglint.New(&sloglint.Options{NoGlobal: "default"})
analysistest.Run(t, testdata, analyzer, "no_global_default")
})

t.Run("context only", func(t *testing.T) {
analyzer := sloglint.New(&sloglint.Options{ContextOnly: true})
analysistest.Run(t, testdata, analyzer, "context_only")
Expand Down
10 changes: 10 additions & 0 deletions testdata/src/no_global_all/no_global_all.go
@@ -0,0 +1,10 @@
package no_global_all

import "log/slog"

var logger = slog.New(nil)

func tests() {
slog.Info("msg") // want `global logger should not be used`
logger.Info("msg") // want `global logger should not be used`
}
10 changes: 10 additions & 0 deletions testdata/src/no_global_default/no_global_default.go
@@ -0,0 +1,10 @@
package no_global_default

import "log/slog"

var logger = slog.New(nil)

func tests() {
slog.Info("msg") // want `default logger should not be used`
logger.Info("msg")
}