diff --git a/linters/stringlint/Makefile b/linters/stringlint/Makefile new file mode 100644 index 0000000..d126544 --- /dev/null +++ b/linters/stringlint/Makefile @@ -0,0 +1,11 @@ +.PHONY: test lint build + +test: + go test -v ./... + +lint: + revive -formatter friendly ./... + go vet ./... + +build: + go build ./... diff --git a/linters/stringlint/README.md b/linters/stringlint/README.md new file mode 100644 index 0000000..e22da40 --- /dev/null +++ b/linters/stringlint/README.md @@ -0,0 +1,127 @@ +# stringlint + +A Go analyzer that detects direct string comparison patterns and recommends using `github.com/wego/pkg/strings` utility functions. + +## Installation + +### Automatic way (recommended) + +This follows golangci-lint's "Automatic Way" module plugin flow. + +Requirements: Go and git. + +1. Create `.custom-gcl.yml` in your project: + +```yaml +version: v2.8.0 +plugins: + - module: github.com/wego/pkg/linters/stringlint + version: v0.1.0 +``` + +2. Build custom golangci-lint: + +```bash +golangci-lint custom +``` + +3. Configure the plugin in `.golangci.yml`: + +```yaml +version: "2" + +linters: + enable: + - stringlint + settings: + custom: + stringlint: + type: "module" + description: "Enforces wego/pkg/strings usage" +``` + +4. Run the resulting custom binary: + +```bash +./custom-gcl run ./... +``` + +### As a standalone tool + +```bash +go install github.com/wego/pkg/linters/stringlint/cmd/stringlint@latest +stringlint ./... +``` + +## What it detects + +| Pattern | Suggestion | +|---------|------------| +| `s == ""` | `wegostrings.IsEmpty(s)` | +| `s != ""` | `wegostrings.IsNotEmpty(s)` | +| `len(s) == 0` | `wegostrings.IsEmpty(s)` | +| `len(s) != 0` | `wegostrings.IsNotEmpty(s)` | +| `len(s) > 0` | `wegostrings.IsNotEmpty(s)` | +| `len(s) >= 1` | `wegostrings.IsNotEmpty(s)` | +| `len(s) < 1` | `wegostrings.IsEmpty(s)` | +| `len(s) <= 0` | `wegostrings.IsEmpty(s)` | +| `*ptr == ""` | `wegostrings.IsEmptyP(ptr)` | +| `*ptr != ""` | `wegostrings.IsNotEmptyP(ptr)` | + +Reversed comparisons with the literal on the left are also detected, for example: +`0 == len(s)`, `0 != len(s)`, `0 < len(s)`, `0 >= len(s)`, `1 <= len(s)`. + +## Import Convention + +The linter uses the alias `wegostrings` to avoid conflict with the stdlib `strings` package: + +```go +import wegostrings "github.com/wego/pkg/strings" + +// The linter suggests: +if wegostrings.IsEmpty(s) { ... } +``` + +## Auto-fix + +The linter provides suggested fixes that can be applied automatically: + +```bash +# With golangci-lint +./custom-gcl run --fix ./... + +# With standalone tool +stringlint -fix ./... +``` + +**Note**: Auto-fix replaces the comparison but does not add the import statement. You will need to: +1. Run `goimports` to add missing imports +2. Ensure the import uses the `wegostrings` alias + +## Development + +### Local tests + +The testdata directory is a standalone module. Run tests from the module root: + +```bash +go test -v ./... +``` + +### Using a commit before tagging + +If you need to consume an untagged commit from another repo, use a Go pseudo-version +instead of a raw SHA. + +```bash +go list -m -json github.com/wego/pkg/linters/stringlint@ +``` + +Then use the returned `Version` value in `.custom-gcl.yml`: + +```yaml +version: v2.8.0 +plugins: + - module: github.com/wego/pkg/linters/stringlint + version: v0.0.0-20260120hhmmss-abcdef123456 +``` diff --git a/linters/stringlint/analyzer.go b/linters/stringlint/analyzer.go new file mode 100644 index 0000000..3ab7327 --- /dev/null +++ b/linters/stringlint/analyzer.go @@ -0,0 +1,56 @@ +// Package stringlint provides a Go analyzer that detects direct string +// comparison patterns and recommends using github.com/wego/pkg/strings +// utility functions instead. +package stringlint + +import ( + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// Analyzer is the stringlint analyzer that checks for direct string comparisons. +var Analyzer = &analysis.Analyzer{ + Name: "stringlint", + Doc: "recommends using github.com/wego/pkg/strings functions over direct string comparisons", + URL: "https://github.com/wego/pkg/linters/stringlint", + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, +} + +func run(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.BinaryExpr)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + binExpr := n.(*ast.BinaryExpr) + + // Only check comparison operators. + switch binExpr.Op { + case token.EQL, token.NEQ, token.GTR, token.GEQ, token.LSS, token.LEQ: + // continue + default: + return + } + + // Check for empty string comparison: s == "" or s != "". + if result := checkEmptyStringComparison(pass, binExpr); result != nil { + reportDiagnostic(pass, binExpr, result) + return + } + + // Check for len comparison: len(s) == 0 or len(s) > 0. + if result := checkLenComparison(pass, binExpr); result != nil { + reportDiagnostic(pass, binExpr, result) + return + } + }) + + return nil, nil +} diff --git a/linters/stringlint/analyzer_test.go b/linters/stringlint/analyzer_test.go new file mode 100644 index 0000000..8e76c8c --- /dev/null +++ b/linters/stringlint/analyzer_test.go @@ -0,0 +1,19 @@ +package stringlint_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + + "github.com/wego/pkg/linters/stringlint" +) + +func TestAnalyzer(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, stringlint.Analyzer, "./example") +} + +func TestAnalyzerWithFixes(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, stringlint.Analyzer, "./example") +} diff --git a/linters/stringlint/cmd/stringlint/main.go b/linters/stringlint/cmd/stringlint/main.go new file mode 100644 index 0000000..a43b84a --- /dev/null +++ b/linters/stringlint/cmd/stringlint/main.go @@ -0,0 +1,12 @@ +// Command stringlint runs the stringlint analyzer. +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + + "github.com/wego/pkg/linters/stringlint" +) + +func main() { + singlechecker.Main(stringlint.Analyzer) +} diff --git a/linters/stringlint/go.mod b/linters/stringlint/go.mod new file mode 100644 index 0000000..41ab4e2 --- /dev/null +++ b/linters/stringlint/go.mod @@ -0,0 +1,13 @@ +module github.com/wego/pkg/linters/stringlint + +go 1.24 + +require ( + github.com/golangci/plugin-module-register v0.1.2 + golang.org/x/tools v0.32.0 +) + +require ( + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.13.0 // indirect +) diff --git a/linters/stringlint/go.sum b/linters/stringlint/go.sum new file mode 100644 index 0000000..e232857 --- /dev/null +++ b/linters/stringlint/go.sum @@ -0,0 +1,10 @@ +github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= diff --git a/linters/stringlint/patterns.go b/linters/stringlint/patterns.go new file mode 100644 index 0000000..cfd6c78 --- /dev/null +++ b/linters/stringlint/patterns.go @@ -0,0 +1,249 @@ +package stringlint + +import ( + "bytes" + "fmt" + "go/ast" + "go/printer" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" +) + +// checkResult holds the result of a pattern check. +type checkResult struct { + expr ast.Expr // The expression to replace (e.g., the variable or *ptr). + isPointer bool // Whether expr is a pointer dereference. + isEmptyTest bool // true for == "", false for != "". +} + +// checkEmptyStringComparison checks for patterns like: +// - s == "" -> IsEmpty(s) +// - s != "" -> IsNotEmpty(s) +// - *ptr == "" -> IsEmptyP(ptr) +// - *ptr != "" -> IsNotEmptyP(ptr) +func checkEmptyStringComparison(pass *analysis.Pass, binExpr *ast.BinaryExpr) *checkResult { + switch binExpr.Op { + case token.EQL, token.NEQ: + // continue + default: + return nil + } + + var strExpr ast.Expr + var emptyLit *ast.BasicLit + + // Check both orderings: s == "" or "" == s. + if lit, ok := binExpr.Y.(*ast.BasicLit); ok && isEmptyStringLit(lit) { + strExpr = binExpr.X + emptyLit = lit + } else if lit, ok := binExpr.X.(*ast.BasicLit); ok && isEmptyStringLit(lit) { + strExpr = binExpr.Y + emptyLit = lit + } + + if emptyLit == nil { + return nil + } + + // Check if it's a pointer dereference. + isPointer := false + exprToReport := strExpr + + if starExpr, ok := strExpr.(*ast.StarExpr); ok { + // *ptr == "" + innerType := pass.TypesInfo.TypeOf(starExpr.X) + if innerType == nil { + return nil + } + if ptr, ok := innerType.Underlying().(*types.Pointer); ok { + if basic, ok := ptr.Elem().Underlying().(*types.Basic); ok && basic.Kind() == types.String { + isPointer = true + exprToReport = starExpr.X // Report on ptr, not *ptr. + } + } + } else { + // s == "" + t := pass.TypesInfo.TypeOf(strExpr) + if t == nil { + return nil + } + basic, ok := t.Underlying().(*types.Basic) + if !ok || basic.Kind() != types.String { + return nil + } + } + + return &checkResult{ + expr: exprToReport, + isPointer: isPointer, + isEmptyTest: binExpr.Op == token.EQL, + } +} + +// checkLenComparison checks for patterns like: +// - len(s) == 0 -> IsEmpty(s) +// - len(s) != 0 -> IsNotEmpty(s) +// - len(s) > 0 -> IsNotEmpty(s) +// - len(s) < 1 -> IsEmpty(s) +// - 0 == len(s) -> IsEmpty(s) (reversed) +func checkLenComparison(pass *analysis.Pass, binExpr *ast.BinaryExpr) *checkResult { + var lenCall *ast.CallExpr + var numLit *ast.BasicLit + var reversed bool + + // Check both orderings: len(s) == 0 or 0 == len(s). + if call, ok := binExpr.X.(*ast.CallExpr); ok { + if isLenCall(call) { + lenCall = call + if lit, ok := binExpr.Y.(*ast.BasicLit); ok { + numLit = lit + } + } + } + if lenCall == nil { + if call, ok := binExpr.Y.(*ast.CallExpr); ok { + if isLenCall(call) { + lenCall = call + reversed = true + if lit, ok := binExpr.X.(*ast.BasicLit); ok { + numLit = lit + } + } + } + } + + if lenCall == nil || numLit == nil || len(lenCall.Args) != 1 { + return nil + } + + // Check if comparing to 0 or 1. + if numLit.Kind != token.INT { + return nil + } + + arg := lenCall.Args[0] + t := pass.TypesInfo.TypeOf(arg) + if t == nil { + return nil + } + + // Only handle string types (not slices, maps, etc.). + basic, ok := t.Underlying().(*types.Basic) + if !ok || basic.Kind() != types.String { + return nil + } + + // Determine if this is an empty test or not-empty test. + isEmptyTest := false + op := binExpr.Op + + // Handle reversed comparisons (0 == len(s) vs len(s) == 0). + if reversed { + switch op { + case token.LSS: // 0 < len(s) means not empty. + op = token.GTR + case token.LEQ: // 0 <= len(s) means len(s) >= 0. + op = token.GEQ + case token.GTR: // 0 > len(s) means empty (always false, but handle it). + op = token.LSS + case token.GEQ: // 0 >= len(s) means empty. + op = token.LEQ + } + } + + switch { + case op == token.EQL && numLit.Value == "0": + // len(s) == 0 + isEmptyTest = true + case op == token.NEQ && numLit.Value == "0": + // len(s) != 0 + isEmptyTest = false + case op == token.GTR && numLit.Value == "0": + // len(s) > 0 + isEmptyTest = false + case op == token.GEQ && numLit.Value == "1": + // len(s) >= 1 + isEmptyTest = false + case op == token.LSS && numLit.Value == "1": + // len(s) < 1 + isEmptyTest = true + case op == token.LEQ && numLit.Value == "0": + // len(s) <= 0 + isEmptyTest = true + default: + return nil + } + + return &checkResult{ + expr: arg, + isPointer: false, // len() doesn't work with pointers. + isEmptyTest: isEmptyTest, + } +} + +// isEmptyStringLit checks if the literal is an empty string "". +func isEmptyStringLit(lit *ast.BasicLit) bool { + return lit.Kind == token.STRING && (lit.Value == `""` || lit.Value == "``") +} + +// isLenCall checks if the call expression is a call to the builtin len(). +func isLenCall(call *ast.CallExpr) bool { + ident, ok := call.Fun.(*ast.Ident) + return ok && ident.Name == "len" +} + +// pkgAlias is the recommended import alias for github.com/wego/pkg/strings +// to avoid conflict with the stdlib "strings" package. +const pkgAlias = "wegostrings" + +// reportDiagnostic reports the diagnostic with a suggested fix. +func reportDiagnostic(pass *analysis.Pass, binExpr *ast.BinaryExpr, result *checkResult) { + funcName := getSuggestedFunc(result) + exprStr := render(pass.Fset, result.expr) + + // Build the replacement: wegostrings.IsEmpty(s) or wegostrings.IsEmptyP(ptr). + replacement := fmt.Sprintf("%s.%s(%s)", pkgAlias, funcName, exprStr) + + pass.Report(analysis.Diagnostic{ + Pos: binExpr.Pos(), + End: binExpr.End(), + Message: fmt.Sprintf("use %s.%s(%s) instead of direct comparison (import %s \"github.com/wego/pkg/strings\")", pkgAlias, funcName, exprStr, pkgAlias), + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Replace with %s.%s(%s)", pkgAlias, funcName, exprStr), + TextEdits: []analysis.TextEdit{ + { + Pos: binExpr.Pos(), + End: binExpr.End(), + NewText: []byte(replacement), + }, + }, + }, + }, + }) +} + +// getSuggestedFunc returns the appropriate function name based on the check result. +func getSuggestedFunc(result *checkResult) string { + if result.isPointer { + if result.isEmptyTest { + return "IsEmptyP" + } + return "IsNotEmptyP" + } + if result.isEmptyTest { + return "IsEmpty" + } + return "IsNotEmpty" +} + +// render renders an AST node to a string. +func render(fset *token.FileSet, node any) string { + var buf bytes.Buffer + if err := printer.Fprint(&buf, fset, node); err != nil { + return "" + } + return buf.String() +} diff --git a/linters/stringlint/plugin.go b/linters/stringlint/plugin.go new file mode 100644 index 0000000..3b1920d --- /dev/null +++ b/linters/stringlint/plugin.go @@ -0,0 +1,28 @@ +package stringlint + +import ( + "github.com/golangci/plugin-module-register/register" + "golang.org/x/tools/go/analysis" +) + +func init() { + register.Plugin(Analyzer.Name, New) +} + +// New is the entry point for the golangci-lint module plugin system. +// The signature must be: func New(any) (register.LinterPlugin, error). +func New(_ any) (register.LinterPlugin, error) { + // conf contains settings from .golangci.yml if any. + // Currently no configuration options are supported. + return plugin{}, nil +} + +type plugin struct{} + +func (plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) { + return []*analysis.Analyzer{Analyzer}, nil +} + +func (plugin) GetLoadMode() string { + return register.LoadModeTypesInfo +} diff --git a/linters/stringlint/testdata/example/empty_string.go b/linters/stringlint/testdata/example/empty_string.go new file mode 100644 index 0000000..c7f2182 --- /dev/null +++ b/linters/stringlint/testdata/example/empty_string.go @@ -0,0 +1,30 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func emptyStringChecks(s string) { + // These should trigger warnings + if s == "" { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("empty") + } + + if s != "" { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("not empty") + } + + if "" == s { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("empty reversed") + } + + if "" != s { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("not empty reversed") + } + + // Backtick empty string + if s == `` { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("backtick empty") + } +} + +// Ensure wegostrings package is used (for golden file) +var _ = wegostrings.IsEmpty diff --git a/linters/stringlint/testdata/example/empty_string.go.golden b/linters/stringlint/testdata/example/empty_string.go.golden new file mode 100644 index 0000000..d21d274 --- /dev/null +++ b/linters/stringlint/testdata/example/empty_string.go.golden @@ -0,0 +1,30 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func emptyStringChecks(s string) { + // These should trigger warnings + if wegostrings.IsEmpty(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("empty") + } + + if wegostrings.IsNotEmpty(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("not empty") + } + + if wegostrings.IsEmpty(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("empty reversed") + } + + if wegostrings.IsNotEmpty(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("not empty reversed") + } + + // Backtick empty string + if wegostrings.IsEmpty(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("backtick empty") + } +} + +// Ensure wegostrings package is used (for golden file) +var _ = wegostrings.IsEmpty diff --git a/linters/stringlint/testdata/example/len_check.go b/linters/stringlint/testdata/example/len_check.go new file mode 100644 index 0000000..c8ff595 --- /dev/null +++ b/linters/stringlint/testdata/example/len_check.go @@ -0,0 +1,37 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func lenChecks(s string) { + // These should trigger warnings + if len(s) == 0 { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("len empty") + } + + if len(s) != 0 { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len not empty") + } + + if len(s) > 0 { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len greater") + } + + if len(s) >= 1 { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len gte 1") + } + + if 1 <= len(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len gte 1 reversed") + } + + if len(s) < 1 { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("len lt 1") + } + + if 0 == len(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("reversed") + } +} + +// Ensure wegostrings package is used (for golden file) +var _ = wegostrings.IsEmpty diff --git a/linters/stringlint/testdata/example/len_check.go.golden b/linters/stringlint/testdata/example/len_check.go.golden new file mode 100644 index 0000000..5908035 --- /dev/null +++ b/linters/stringlint/testdata/example/len_check.go.golden @@ -0,0 +1,37 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func lenChecks(s string) { + // These should trigger warnings + if wegostrings.IsEmpty(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("len empty") + } + + if wegostrings.IsNotEmpty(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len not empty") + } + + if wegostrings.IsNotEmpty(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len greater") + } + + if wegostrings.IsNotEmpty(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len gte 1") + } + + if wegostrings.IsNotEmpty(s) { // want `use wegostrings.IsNotEmpty\(s\) instead of direct comparison` + println("len gte 1 reversed") + } + + if wegostrings.IsEmpty(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("len lt 1") + } + + if wegostrings.IsEmpty(s) { // want `use wegostrings.IsEmpty\(s\) instead of direct comparison` + println("reversed") + } +} + +// Ensure wegostrings package is used (for golden file) +var _ = wegostrings.IsEmpty diff --git a/linters/stringlint/testdata/example/pointer.go b/linters/stringlint/testdata/example/pointer.go new file mode 100644 index 0000000..841e781 --- /dev/null +++ b/linters/stringlint/testdata/example/pointer.go @@ -0,0 +1,21 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func pointerChecks(ptr *string) { + // These should trigger warnings with P-suffix functions + if *ptr == "" { // want `use wegostrings.IsEmptyP\(ptr\) instead of direct comparison` + println("ptr empty") + } + + if *ptr != "" { // want `use wegostrings.IsNotEmptyP\(ptr\) instead of direct comparison` + println("ptr not empty") + } + + if "" == *ptr { // want `use wegostrings.IsEmptyP\(ptr\) instead of direct comparison` + println("ptr empty reversed") + } +} + +// Ensure wegostrings package is used (for golden file) +var _ = wegostrings.IsEmptyP diff --git a/linters/stringlint/testdata/example/pointer.go.golden b/linters/stringlint/testdata/example/pointer.go.golden new file mode 100644 index 0000000..06b0cc1 --- /dev/null +++ b/linters/stringlint/testdata/example/pointer.go.golden @@ -0,0 +1,21 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func pointerChecks(ptr *string) { + // These should trigger warnings with P-suffix functions + if wegostrings.IsEmptyP(ptr) { // want `use wegostrings.IsEmptyP\(ptr\) instead of direct comparison` + println("ptr empty") + } + + if wegostrings.IsNotEmptyP(ptr) { // want `use wegostrings.IsNotEmptyP\(ptr\) instead of direct comparison` + println("ptr not empty") + } + + if wegostrings.IsEmptyP(ptr) { // want `use wegostrings.IsEmptyP\(ptr\) instead of direct comparison` + println("ptr empty reversed") + } +} + +// Ensure wegostrings package is used (for golden file) +var _ = wegostrings.IsEmptyP diff --git a/linters/stringlint/testdata/example/valid.go b/linters/stringlint/testdata/example/valid.go new file mode 100644 index 0000000..78713ca --- /dev/null +++ b/linters/stringlint/testdata/example/valid.go @@ -0,0 +1,55 @@ +package example + +import wegostrings "github.com/wego/pkg/strings" + +func validCode(s string, ptr *string) { + // These should NOT trigger warnings + + // Already using the recommended functions + if wegostrings.IsEmpty(s) { + println("good") + } + if wegostrings.IsNotEmpty(s) { + println("good") + } + if wegostrings.IsEmptyP(ptr) { + println("good") + } + + // Comparing to non-empty strings - this is intentional + if s == "hello" { + println("specific comparison is fine") + } + if s != "world" { + println("specific comparison is fine") + } + + // len() on slices - not our concern + slice := []int{1, 2, 3} + if len(slice) == 0 { + println("slice is empty") + } + + // len() on maps - not our concern + m := map[string]int{} + if len(m) == 0 { + println("map is empty") + } + + // Comparisons that aren't empty checks + if len(s) == 5 { + println("length is 5") + } + if len(s) > 10 { + println("length > 10") + } + + // String comparison between two variables + other := "test" + if s == other { + println("comparing two strings") + } +} + +// Ensure wegostrings package is used +var _ = wegostrings.IsEmpty diff --git a/linters/stringlint/testdata/go.mod b/linters/stringlint/testdata/go.mod new file mode 100644 index 0000000..26b8128 --- /dev/null +++ b/linters/stringlint/testdata/go.mod @@ -0,0 +1,7 @@ +module example + +go 1.24 + +require github.com/wego/pkg/strings v0.1.1 + +replace github.com/wego/pkg/strings => ../../../strings