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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ func flaggedExamples() {
_ = "alice" == strings.ToLower(name) // want `use strings\.EqualFold`
_ = strings.ToLower(name) != "alice" // want `use strings\.EqualFold`
_ = strings.ToLower(name) == strings.ToLower("alice") // want `use strings\.EqualFold`

lower := strings.ToLower(name)
_ = lower == "alice" // want `use strings\.EqualFold`
Comment on lines +16 to +17
}

func okExamples() {
Expand All @@ -25,6 +28,9 @@ func okExamples() {
_ = strings.ToLower(name) // used standalone, not in a comparison
_ = strings.ToLower(name) == name
_ = strings.ToLower(name) != name

lower := strings.ToLower(name)
_ = lower == name
}

func suppressedExamples() {
Expand Down
151 changes: 150 additions & 1 deletion pkg/linters/tolowerequalfold/tolowerequalfold.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"go/ast"
"go/token"
"go/types"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
Expand All @@ -31,6 +32,7 @@ func run(pass *analysis.Pass) (any, error) {
return nil, fmt.Errorf("inspect analyzer result has unexpected type %T", pass.ResultOf[inspect.Analyzer])
}
noLintLinesByFile := nolint.BuildLineIndex(pass, "tolowerequalfold")
caseConvAliases := collectCaseConvAliases(pass)

nodeFilter := []ast.Node{
(*ast.BinaryExpr)(nil),
Expand All @@ -55,8 +57,16 @@ func run(pass *analysis.Pass) (any, error) {
if arg, ok := caseConvArg(expr.Y); ok && sameOperand(pass, expr.X, arg) {
return
}
if arg, ok := caseConvAliasArg(pass, expr.X, caseConvAliases); ok && sameOperand(pass, arg, expr.Y) {
return
}
if arg, ok := caseConvAliasArg(pass, expr.Y, caseConvAliases); ok && sameOperand(pass, expr.X, arg) {
return
}

if isCaseConvCall(expr.X) || isCaseConvCall(expr.Y) {
if isCaseConvCall(expr.X) || isCaseConvCall(expr.Y) ||
(isCaseConvAlias(pass, expr.X, caseConvAliases) && isStringLiteral(expr.Y)) ||
(isCaseConvAlias(pass, expr.Y, caseConvAliases) && isStringLiteral(expr.X)) {
if nolint.HasDirective(pass.Fset.PositionFor(expr.Pos(), false), noLintLinesByFile) {
return
}
Expand All @@ -68,12 +78,151 @@ func run(pass *analysis.Pass) (any, error) {
return nil, nil
}

func collectCaseConvAliases(pass *analysis.Pass) map[types.Object]ast.Expr {
aliases := make(map[types.Object]ast.Expr)
for _, file := range pass.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.AssignStmt:
collectAliasesFromAssignStmt(pass, n, aliases)
case *ast.ValueSpec:
collectAliasesFromValueSpec(pass, n, aliases)
Comment on lines +81 to +89
case *ast.IncDecStmt:
if ident, ok := n.X.(*ast.Ident); ok {
delete(aliases, pass.TypesInfo.ObjectOf(ident))
}
case *ast.RangeStmt:
if n.Tok == token.ASSIGN {
deleteAliasForExpr(pass, aliases, n.Key)
deleteAliasForExpr(pass, aliases, n.Value)
}
}
return true
})
}
return aliases
}

func collectAliasesFromAssignStmt(pass *analysis.Pass, stmt *ast.AssignStmt, aliases map[types.Object]ast.Expr) {
for i, lhs := range stmt.Lhs {
ident, ok := lhs.(*ast.Ident)
if !ok || ident.Name == "_" {
continue
}
obj := pass.TypesInfo.ObjectOf(ident)
if obj == nil || !isLocalObject(obj) {
continue
}

switch stmt.Tok {
case token.DEFINE:
if obj.Pos() != ident.Pos() {
delete(aliases, obj)
continue
}
rhs, ok := rhsExprForIndex(stmt.Rhs, i)
if !ok {
delete(aliases, obj)
continue
}
if arg, ok := caseConvArg(rhs); ok {
aliases[obj] = arg
} else {
delete(aliases, obj)
}
case token.ASSIGN:
delete(aliases, obj)
}
Comment on lines +117 to +135
}
}

func collectAliasesFromValueSpec(pass *analysis.Pass, spec *ast.ValueSpec, aliases map[types.Object]ast.Expr) {
for i, name := range spec.Names {
if name.Name == "_" {
continue
}
obj := pass.TypesInfo.ObjectOf(name)
if obj == nil || !isLocalObject(obj) {
continue
}
rhs, ok := rhsExprForIndex(spec.Values, i)
if !ok {
delete(aliases, obj)
continue
}
if arg, ok := caseConvArg(rhs); ok {
aliases[obj] = arg
} else {
delete(aliases, obj)
}
}
}

func rhsExprForIndex(rhs []ast.Expr, idx int) (ast.Expr, bool) {
switch {
case len(rhs) == 0:
return nil, false
case len(rhs) == 1 && idx == 0:
return rhs[0], true
case idx < len(rhs):
return rhs[idx], true
default:
return nil, false
}
}

func deleteAliasForExpr(pass *analysis.Pass, aliases map[types.Object]ast.Expr, expr ast.Expr) {
ident, ok := expr.(*ast.Ident)
if !ok {
return
}
delete(aliases, pass.TypesInfo.ObjectOf(ident))
}

// isCaseConvCall reports whether node is a call to strings.ToLower or strings.ToUpper.
func isCaseConvCall(n ast.Node) bool {
_, ok := caseConvArg(n)
return ok
}

func isCaseConvAlias(pass *analysis.Pass, expr ast.Expr, aliases map[types.Object]ast.Expr) bool {
_, ok := caseConvAliasArg(pass, expr, aliases)
return ok
}

func caseConvAliasArg(pass *analysis.Pass, expr ast.Expr, aliases map[types.Object]ast.Expr) (ast.Expr, bool) {
ident, ok := expr.(*ast.Ident)
if !ok {
return nil, false
}
obj := pass.TypesInfo.ObjectOf(ident)
if obj == nil {
return nil, false
}
arg, ok := aliases[obj]
if !ok {
return nil, false
}
return arg, true
}

func isStringLiteral(expr ast.Expr) bool {
lit, ok := expr.(*ast.BasicLit)
return ok && lit.Kind == token.STRING
}

func isLocalObject(obj types.Object) bool {
if obj == nil {
return false
}
parent := obj.Parent()
if parent == nil {
return false
}
pkg := obj.Pkg()
return pkg == nil || parent != pkg.Scope()
}

// caseConvArg returns the argument when n is strings.ToLower/ToUpper(<arg>).
func caseConvArg(n ast.Node) (ast.Expr, bool) {
call, ok := n.(*ast.CallExpr)
Expand Down
3 changes: 1 addition & 2 deletions pkg/parser/yaml_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ func isActionDefinitionFile(filePath string, content []byte) (bool, error) {
// Supports both .yml and .yaml extensions for consistency with GitHub Actions
func isCopilotSetupStepsFile(filePath string) bool {
base := filepath.Base(filePath)
lower := strings.ToLower(base)
return lower == "copilot-setup-steps.yml" || lower == "copilot-setup-steps.yaml"
return strings.EqualFold(base, "copilot-setup-steps.yml") || strings.EqualFold(base, "copilot-setup-steps.yaml")
}

// processYAMLWorkflowImport processes an imported YAML workflow file
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/error_recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func classifyValidationSeverity(field string, reason string) (ErrorSeverity, str
lowerReason := strings.ToLower(reason)

switch {
case lowerField == "engine" || strings.Contains(lowerReason, "invalid engine"):
case strings.EqualFold(lowerField, "engine") || strings.Contains(lowerReason, "invalid engine"):
return SeverityCritical, "configuration"
case strings.Contains(lowerField, "network") || strings.Contains(lowerReason, "strict mode"):
return SeverityHigh, "permissions"
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func isFeatureEnabled(flag constants.FeatureFlag, workflowData *WorkflowData) bo

// Inline sub-agents are now enabled by default and the corresponding
// frontmatter flag is deprecated/no-op.
if flagLower == "inline-agents" {
if strings.EqualFold(flagLower, "inline-agents") {
if logEnabled {
featuresLog.Printf("Feature %s is deprecated and always enabled", flagLower)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/runs_on_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func validateRunsOn(frontmatter map[string]any, markdownPath string) error {
labels := extractRunnerLabels(runsOn)
for _, label := range labels {
lower := strings.ToLower(label)
if strings.HasPrefix(lower, "macos-") || lower == "macos" {
if strings.HasPrefix(lower, "macos-") || strings.EqualFold(lower, "macos") {
return formatCompilerError(markdownPath, "error",
fmt.Sprintf("runner '%s' is not supported in agentic workflows.\n\n"+
"macOS runners are not supported because agentic workflows rely on containers "+
Expand Down