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
41 changes: 28 additions & 13 deletions pkg/linters/lenstringzero/lenstringzero.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func analyzeLenStringExpr(pass *analysis.Pass, n ast.Node, generatedFiles filech
if nolint.HasDirectiveForLinter(pos, noLintIndex, "lenstringzero") {
return
}
lenArg, isDirect, normalOp, lit, matched := matchLenLiteralExpr(pass, expr, lenStringAliases)
lenArg, lenNode, isDirect, normalOp, lit, matched := matchLenLiteralExpr(pass, expr, lenStringAliases)
if !matched {
return
}
Expand All @@ -67,70 +67,73 @@ func analyzeLenStringExpr(pass *analysis.Pass, n ast.Node, generatedFiles filech
if !ok || basic.Kind() != types.String {
return
}
argText := exprTextOr(pass, lenArg, "s")
lenText := exprTextOr(pass, lenNode, "len(s)")
var fixes []analysis.SuggestedFix
if isDirect {
fixes = buildLenStringFix(pass, expr, lenArg, fixOp)
}
pass.Report(analysis.Diagnostic{
Pos: expr.Pos(),
End: expr.End(),
Message: fmt.Sprintf(`use s %s "" to check for %s string instead of len(s) %s %d`, fixOp, cmpVerb, normalOp, lit),
Message: fmt.Sprintf(`use %s %s "" to check for %s string instead of %s %s %d`, argText, fixOp, cmpVerb, lenText, normalOp, lit),
SuggestedFixes: fixes,
})
}

// matchLenLiteralExpr tries to match len(s)/alias OP literal or literal OP len(s)/alias.
// Returns (lenArg, isDirect, normalOp, lit, matched) where:
// Returns (lenArg, lenNode, isDirect, normalOp, lit, matched) where:
// - lenArg is the string expression passed to len()
// - lenNode is the expression being compared (the len() call or the alias identifier)
// - isDirect indicates a direct len() call (true) vs a stored alias (false)
// - normalOp is the operator normalized so that len is on the left side
// - lit is the integer literal value (0 or 1)
// - matched indicates whether a valid pattern was found
func matchLenLiteralExpr(pass *analysis.Pass, expr *ast.BinaryExpr, aliases map[types.Object]ast.Expr) (lenArg ast.Expr, isDirect bool, normalOp token.Token, lit int, ok bool) {
func matchLenLiteralExpr(pass *analysis.Pass, expr *ast.BinaryExpr, aliases map[types.Object]ast.Expr) (lenArg ast.Expr, lenNode ast.Expr, isDirect bool, normalOp token.Token, lit int, ok bool) {
op := expr.Op

// Normal order: len/alias on the left, literal on the right.
if isLenCall(expr.X) {
if isIntZero(expr.Y) {
return lenCallArg(expr.X), true, op, 0, true
return lenCallArg(expr.X), expr.X, true, op, 0, true
}
if isIntOne(expr.Y) {
return lenCallArg(expr.X), true, op, 1, true
return lenCallArg(expr.X), expr.X, true, op, 1, true
}
}
if isIntZero(expr.Y) {
if arg, ok2 := lenAliasArg(pass, expr.X, aliases); ok2 {
return arg, false, op, 0, true
return arg, expr.X, false, op, 0, true
}
}
if isIntOne(expr.Y) {
if arg, ok2 := lenAliasArg(pass, expr.X, aliases); ok2 {
return arg, false, op, 1, true
return arg, expr.X, false, op, 1, true
}
}

// Yoda order: literal on the left, len/alias on the right.
// Flip the operator so the normalized form has len on the left.
if isLenCall(expr.Y) {
if isIntZero(expr.X) {
return lenCallArg(expr.Y), true, astutil.FlipComparisonOp(op), 0, true
return lenCallArg(expr.Y), expr.Y, true, astutil.FlipComparisonOp(op), 0, true
}
if isIntOne(expr.X) {
return lenCallArg(expr.Y), true, astutil.FlipComparisonOp(op), 1, true
return lenCallArg(expr.Y), expr.Y, true, astutil.FlipComparisonOp(op), 1, true
}
}
if isIntZero(expr.X) {
if arg, ok2 := lenAliasArg(pass, expr.Y, aliases); ok2 {
return arg, false, astutil.FlipComparisonOp(op), 0, true
return arg, expr.Y, false, astutil.FlipComparisonOp(op), 0, true
}
}
if isIntOne(expr.X) {
if arg, ok2 := lenAliasArg(pass, expr.Y, aliases); ok2 {
return arg, false, astutil.FlipComparisonOp(op), 1, true
return arg, expr.Y, false, astutil.FlipComparisonOp(op), 1, true
}
}

return nil, false, 0, 0, false
return nil, nil, false, 0, 0, false
}

// resolveFixOp returns the fix operator and comparison verb for a normalized
Expand Down Expand Up @@ -195,6 +198,18 @@ func buildLenStringFix(pass *analysis.Pass, expr *ast.BinaryExpr, lenArg ast.Exp
}}
}

// exprTextOr renders node's source text, falling back to fallback when the
// node cannot be printed.
func exprTextOr(pass *analysis.Pass, node ast.Expr, fallback string) string {
if node == nil {
return fallback
}
if text := astutil.NodeText(pass.Fset, node); text != "" {
return text
}
return fallback
}

func isLenCall(expr ast.Expr) bool {
call, ok := expr.(*ast.CallExpr)
if !ok || len(call.Args) != 1 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,32 @@ func lenNotComparedToZero(s string) bool {

func aliasEmpty(s string) bool {
n := len(s)
return n == 0 // want `use s == "" to check for empty string instead of len\(s\) == 0`
return n == 0 // want `use s == "" to check for empty string instead of n == 0`
}

func aliasNotEmpty(s string) bool {
n := len(s)
return n != 0 // want `use s != "" to check for non-empty string instead of len\(s\) != 0`
return n != 0 // want `use s != "" to check for non-empty string instead of n != 0`
}

func aliasGreaterThanZero(s string) bool {
n := len(s)
return n > 0 // want `use s != "" to check for non-empty string instead of len\(s\) > 0`
return n > 0 // want `use s != "" to check for non-empty string instead of n > 0`
}

func aliasGreaterOrEqualOne(s string) bool {
n := len(s)
return n >= 1 // want `use s != "" to check for non-empty string instead of len\(s\) >= 1`
return n >= 1 // want `use s != "" to check for non-empty string instead of n >= 1`
}

func aliasLessThanOne(s string) bool {
n := len(s)
return n < 1 // want `use s == "" to check for empty string instead of len\(s\) < 1`
return n < 1 // want `use s == "" to check for empty string instead of n < 1`
}

func aliasLessOrEqualZero(s string) bool {
n := len(s)
return n <= 0 // want `use s == "" to check for empty string instead of len\(s\) <= 0`
return n <= 0 // want `use s == "" to check for empty string instead of n <= 0`
}

func aliasReassignedNotFlagged(s string) bool {
Expand All @@ -135,6 +135,15 @@ func arrayAliasNotFlagged(s [1]byte) bool {
return n == 0
}

func namedVariableEmpty(username string) bool {
return len(username) == 0 // want `use username == "" to check for empty string instead of len\(username\) == 0`
}

func namedVariableAliasEmpty(username string) bool {
usernameLen := len(username)
return usernameLen == 0 // want `use username == "" to check for empty string instead of usernameLen == 0`
}

func suppressedEmpty(s string) bool {
return len(s) == 0 //nolint:lenstringzero
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,32 +85,32 @@ func lenNotComparedToZero(s string) bool {

func aliasEmpty(s string) bool {
n := len(s)
return n == 0 // want `use s == "" to check for empty string instead of len\(s\) == 0`
return n == 0 // want `use s == "" to check for empty string instead of n == 0`
}

func aliasNotEmpty(s string) bool {
n := len(s)
return n != 0 // want `use s != "" to check for non-empty string instead of len\(s\) != 0`
return n != 0 // want `use s != "" to check for non-empty string instead of n != 0`
}

func aliasGreaterThanZero(s string) bool {
n := len(s)
return n > 0 // want `use s != "" to check for non-empty string instead of len\(s\) > 0`
return n > 0 // want `use s != "" to check for non-empty string instead of n > 0`
}

func aliasGreaterOrEqualOne(s string) bool {
n := len(s)
return n >= 1 // want `use s != "" to check for non-empty string instead of len\(s\) >= 1`
return n >= 1 // want `use s != "" to check for non-empty string instead of n >= 1`
}

func aliasLessThanOne(s string) bool {
n := len(s)
return n < 1 // want `use s == "" to check for empty string instead of len\(s\) < 1`
return n < 1 // want `use s == "" to check for empty string instead of n < 1`
}

func aliasLessOrEqualZero(s string) bool {
n := len(s)
return n <= 0 // want `use s == "" to check for empty string instead of len\(s\) <= 0`
return n <= 0 // want `use s == "" to check for empty string instead of n <= 0`
}

func aliasReassignedNotFlagged(s string) bool {
Expand All @@ -135,6 +135,15 @@ func arrayAliasNotFlagged(s [1]byte) bool {
return n == 0
}

func namedVariableEmpty(username string) bool {
return username == "" // want `use username == "" to check for empty string instead of len\(username\) == 0`
}

func namedVariableAliasEmpty(username string) bool {
usernameLen := len(username)
return usernameLen == 0 // want `use username == "" to check for empty string instead of usernameLen == 0`
}

func suppressedEmpty(s string) bool {
return len(s) == 0 //nolint:lenstringzero
}