From 8cf5e4c2a022b73ecec54ceea2c7bc66c5394ae0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 05:32:16 +0000 Subject: [PATCH 1/2] Initial plan From 600630288e17da0075f7753f6da861d98271f43a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 05:44:48 +0000 Subject: [PATCH 2/2] fix tolowerequalfold alias detection and EqualFold call sites Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../src/tolowerequalfold/tolowerequalfold.go | 6 + .../tolowerequalfold/tolowerequalfold.go | 151 +++++++++++++++++- pkg/parser/yaml_import.go | 3 +- pkg/workflow/error_recovery.go | 2 +- pkg/workflow/features.go | 2 +- pkg/workflow/runs_on_validation.go | 2 +- 6 files changed, 160 insertions(+), 6 deletions(-) diff --git a/pkg/linters/tolowerequalfold/testdata/src/tolowerequalfold/tolowerequalfold.go b/pkg/linters/tolowerequalfold/testdata/src/tolowerequalfold/tolowerequalfold.go index a615113d54f..96e9b9df8ac 100644 --- a/pkg/linters/tolowerequalfold/testdata/src/tolowerequalfold/tolowerequalfold.go +++ b/pkg/linters/tolowerequalfold/testdata/src/tolowerequalfold/tolowerequalfold.go @@ -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` } func okExamples() { @@ -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() { diff --git a/pkg/linters/tolowerequalfold/tolowerequalfold.go b/pkg/linters/tolowerequalfold/tolowerequalfold.go index 37340c1bbc5..c19144bd72a 100644 --- a/pkg/linters/tolowerequalfold/tolowerequalfold.go +++ b/pkg/linters/tolowerequalfold/tolowerequalfold.go @@ -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" @@ -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), @@ -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 } @@ -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) + 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) + } + } +} + +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(). func caseConvArg(n ast.Node) (ast.Expr, bool) { call, ok := n.(*ast.CallExpr) diff --git a/pkg/parser/yaml_import.go b/pkg/parser/yaml_import.go index a45bdccce38..2e1d7c74d26 100644 --- a/pkg/parser/yaml_import.go +++ b/pkg/parser/yaml_import.go @@ -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 diff --git a/pkg/workflow/error_recovery.go b/pkg/workflow/error_recovery.go index c0f2368b0f1..4a3c2de8fdb 100644 --- a/pkg/workflow/error_recovery.go +++ b/pkg/workflow/error_recovery.go @@ -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" diff --git a/pkg/workflow/features.go b/pkg/workflow/features.go index f2a5f27afcb..ad99cbc61fc 100644 --- a/pkg/workflow/features.go +++ b/pkg/workflow/features.go @@ -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) } diff --git a/pkg/workflow/runs_on_validation.go b/pkg/workflow/runs_on_validation.go index c160ff8bf34..eb388d1b450 100644 --- a/pkg/workflow/runs_on_validation.go +++ b/pkg/workflow/runs_on_validation.go @@ -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 "+