Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions linters/stringlint/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.PHONY: test lint build

test:
go test -v ./...

lint:
revive -formatter friendly ./...
go vet ./...

build:
go build ./...
127 changes: 127 additions & 0 deletions linters/stringlint/README.md
Original file line number Diff line number Diff line change
@@ -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@<commit>
```

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
```
56 changes: 56 additions & 0 deletions linters/stringlint/analyzer.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 19 additions & 0 deletions linters/stringlint/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
12 changes: 12 additions & 0 deletions linters/stringlint/cmd/stringlint/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 13 additions & 0 deletions linters/stringlint/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
10 changes: 10 additions & 0 deletions linters/stringlint/go.sum
Original file line number Diff line number Diff line change
@@ -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=
Loading
Loading