Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 79 additions & 0 deletions pkg/linters/reflect-deepequal-usage/reflect_deepequal_usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Package reflectdeepequalusage implements a Go analysis linter that flags
// reflect.DeepEqual() usage in conditionals or comparisons that should use
// typed equality operators or type-specific comparison functions for performance
// and type safety.
package reflectdeepequalusage

import (
"fmt"
"go/ast"

"golang.org/x/tools/go/analysis"

"github.com/github/gh-aw/pkg/linters/internal/analyzerutil"
"github.com/github/gh-aw/pkg/linters/internal/astutil"
"github.com/github/gh-aw/pkg/linters/internal/filecheck"
"github.com/github/gh-aw/pkg/linters/internal/nolint"
"github.com/github/gh-aw/pkg/logger"
)

var pkgLog = logger.New("linters:reflectdeepequalusage")

// Analyzer is the reflect-deepequal-usage analysis pass.
var Analyzer = analyzerutil.New(
"reflectdeepequalusage",
"reports reflect.DeepEqual() usage in conditionals or comparisons that should use typed equality operators or type-specific comparison functions for performance and type safety",
run,
)

// URL points to documentation for this linter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Don't hand-roll the Analyzer.URL patch with an IIFE; use the existing analyzerutil.NewAtPath helper, which is built exactly for dash-named package directories (see panic-in-library-code.go).

💡 Suggested fix
var Analyzer = analyzerutil.NewAtPath(
	"reflectdeepequalusage",
	"reports reflect.DeepEqual() usage in conditionals or comparisons that should use typed equality operators or type-specific comparison functions for performance and type safety",
	"reflect-deepequal-usage",
	run,
)

This removes the need for the awkward package-scope var _ = func() struct{} { ... }() block entirely and matches the convention panicinlibrarycode.Analyzer already establishes for the same directory-naming mismatch.

@copilot please address this.

var _ = func() struct{} {
Analyzer.URL = "https://github.com/github/gh-aw/tree/main/pkg/linters/reflect-deepequal-usage"
return struct{}{}
}()

func run(pass *analysis.Pass) (any, error) {
pkgLog.Printf("analyzing package %s", pass.Pkg.Path())

noLintIndex, generatedFiles, err := analyzerutil.Indexes(pass)
if err != nil {
return nil, err
}

nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
return analyzerutil.Preorder(pass, nodeFilter, func(n ast.Node) {
analyzeCall(pass, n, generatedFiles, noLintIndex)
})
}

// analyzeCall checks whether a call is reflect.DeepEqual() and reports a diagnostic if so.
func analyzeCall(pass *analysis.Pass, n ast.Node, generatedFiles filecheck.GeneratedIndex, noLintIndex nolint.DirectiveIndex) {
call, ok := n.(*ast.CallExpr)
if !ok {
return
}

// Get the file position for skipping generated files
pos := pass.Fset.PositionFor(call.Pos(), false)
if filecheck.ShouldSkipFilename(pos.Filename, generatedFiles) {
return
}
if nolint.HasDirectiveForLinter(pos, noLintIndex, "reflectdeepequalusage") {
return
}

// Check if this is a call to reflect.DeepEqual
pkgPath, funcName, ok := astutil.PackageCall(pass, call)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reports every direct reflect.DeepEqual call, so enabling it globally will fail the custom lint run on existing legitimate comparisons that cannot be rewritten as ==.

💡 Narrow the rule before wiring it into allAnalyzers

The implementation currently flags production cases like pkg/cli/edit_command.go, pkg/cli/mcp_config_file.go, and pkg/workflow/plugins_frontmatter.go, where == is either impossible or changes semantics. Please constrain the rule to cases with a provably safe typed replacement, or keep it out of the global registry until those call sites have an explicit migration path.

if !ok || pkgPath != "reflect" || funcName != "DeepEqual" {
return
}

pkgLog.Printf("flagging reflect.DeepEqual() call at %s:%d", pos.Filename, pos.Line)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] fmt.Sprintf is used with a static string and no format arguments — a plain string literal (or pass.Reportf, as sprintferrorsnew.go does) is simpler and avoids an unnecessary import/indirection.

💡 Suggested simplification
pass.Report(analysis.Diagnostic{
	Pos: call.Pos(),
	End: call.End(),
	Message: "reflect.DeepEqual() is inefficient and should not be used in conditionals; prefer typed equality operators or type-specific comparison functions",
})

This also lets you drop the "fmt" import.

@copilot please address this.

pass.Report(analysis.Diagnostic{
Comment on lines +81 to +82
Pos: call.Pos(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L73: shrink: fmt.Sprintf wraps a constant diagnostic. Use the string literal directly and remove fmt.

End: call.End(),
Message: fmt.Sprintf(

Check failure on line 75 in pkg/linters/reflect-deepequal-usage/reflect_deepequal_usage.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (2)

string-format: fmt.Sprintf can be replaced with just using the string (perfsprint)

Check failure on line 75 in pkg/linters/reflect-deepequal-usage/reflect_deepequal_usage.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (1)

S1039: unnecessary use of fmt.Sprintf (staticcheck)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Sprintf is used here with no format verbs or arguments — it's just wrapping a static string. staticcheck flags this as S1039 ("unnecessary use of fmt.Sprintf"), which will fail CI lint checks.

Message: "reflect.DeepEqual() is inefficient and should not be used in conditionals; prefer typed equality operators or type-specific comparison functions",

@copilot please address this.

"reflect.DeepEqual() is inefficient and should not be used in conditionals; prefer typed equality operators or type-specific comparison functions",
),
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !integration

// Package reflectdeepequalusage_test provides tests for the reflect-deepequal-usage analyzer.
package reflectdeepequalusage_test

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"

"github.com/github/gh-aw/pkg/linters/reflect-deepequal-usage"

Check failure on line 11 in pkg/linters/reflect-deepequal-usage/reflect_deepequal_usage_test.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (0)

File is not properly formatted (goimports)

Check failure on line 11 in pkg/linters/reflect-deepequal-usage/reflect_deepequal_usage_test.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (2)

File is not properly formatted (goimports)

Check failure on line 11 in pkg/linters/reflect-deepequal-usage/reflect_deepequal_usage_test.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (1)

File is not properly formatted (goimports)
)

func TestReflectDeepEqualUsage(t *testing.T) {
t.Parallel()
analysistest.Run(t, analysistest.TestData(), reflectdeepequalusage.Analyzer, "a", "b")
}
45 changes: 45 additions & 0 deletions pkg/linters/reflect-deepequal-usage/testdata/src/a/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Package a is the test fixture for positive cases of the reflect-deepequal-usage analyzer.
package a

import (
"reflect"
)

// testDeepEqualInCondition should be flagged - using reflect.DeepEqual in if condition
func testDeepEqualInCondition(a, b interface{}) {
if reflect.DeepEqual(a, b) { // want `reflect.DeepEqual\(\) is inefficient`
println("equal")
}
}

// testDeepEqualInAssignment should be flagged - assigning reflect.DeepEqual result
func testDeepEqualInAssignment(a, b interface{}) bool {
result := reflect.DeepEqual(a, b) // want `reflect.DeepEqual\(\) is inefficient`
return result
}

// testDeepEqualInReturn should be flagged - returning reflect.DeepEqual result
func testDeepEqualInReturn(a, b interface{}) bool {
return reflect.DeepEqual(a, b) // want `reflect.DeepEqual\(\) is inefficient`
}

// testDeepEqualNegation should be flagged - negating reflect.DeepEqual result
func testDeepEqualNegation(a, b interface{}) bool {
return !reflect.DeepEqual(a, b) // want `reflect.DeepEqual\(\) is inefficient`
}

// testDeepEqualWithAlias should be flagged - using an alias for reflect package

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The comment says testDeepEqualWithAlias "should be flagged", but there's no // want annotation on either line, so analysistest asserts zero diagnostics here — and indeed the analyzer only matches direct reflect.DeepEqual(...) calls, not calls through a function-value alias like r := reflect.DeepEqual; r(a, b). This is a real detection gap, not just a missing assertion.

💡 Fix options

Either:

  1. Update the comment to say this case is a known limitation and should NOT be flagged (matching actual/current behavior), or
  2. If aliasing should be caught, extend analyzeCall/astutil.PackageCall to resolve *ast.Ident call targets back to their SelectorExpr origin and add a // want annotation confirming it's detected.

As written the test silently passes while documenting an intent the code doesn't fulfill, which will confuse future maintainers per /tdd's "test names/comments should read as specifications" guidance.

@copilot please address this.

func testDeepEqualWithAlias(a, b interface{}) bool {
r := reflect.DeepEqual // reference to function
return r(a, b)
Comment on lines +33 to +34
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "should be flagged" but there's no // want directive here, so analysistest actually asserts this call is not reported. The linter's PackageCall helper only matches direct pkg.Func(...) selector calls, so r(a, b) (a function value assigned from reflect.DeepEqual) is a real gap/false-negative, not something intentionally skipped.

Please either:

  • fix the comment to say "is NOT flagged (known limitation: aliased function values aren't detected)" and move it to b.go as a negative case, or
  • extend the analyzer to resolve aliased function values if that's in scope.

As written, the comment contradicts the test assertion, which is confusing for future maintainers.

@copilot please address this.


// testDeepEqualInComparison should be flagged - comparing result in expression
func testDeepEqualInComparison(a, b, c, d interface{}) bool {
return reflect.DeepEqual(a, b) && reflect.DeepEqual(c, d) // want `reflect.DeepEqual\(\) is inefficient` `reflect.DeepEqual\(\) is inefficient`
}

// testDeepEqualStandalone should be flagged - standalone call that discards result
func testDeepEqualStandalone(a, b interface{}) {
reflect.DeepEqual(a, b) // want `reflect.DeepEqual\(\) is inefficient`
}
67 changes: 67 additions & 0 deletions pkg/linters/reflect-deepequal-usage/testdata/src/b/b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Package b is the test fixture for negative cases of the reflect-deepequal-usage analyzer.
package b

import (
"reflect"
)

// testTypedEquality should NOT be flagged - using typed equality
func testTypedEquality(a, b int) bool {
return a == b
}

// testStringEquality should NOT be flagged - comparing strings directly
func testStringEquality(a, b string) bool {
return a == b
}

// testSliceEquality should NOT be flagged - comparing slices with other methods
func testSliceEquality(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

// testMapEquality should NOT be flagged - comparing maps without reflect.DeepEqual
func testMapEquality(a, b map[string]int) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if b[k] != v {
return false
}
}
return true
}

// testReflectNotDeepEqual should NOT be flagged - using reflect but different function
func testReflectNotDeepEqual(t interface{}) bool {
return reflect.TypeOf(t) != nil
}

// testReflectValueEqual should NOT be flagged - using reflect.Value.Equal
func testReflectValueEqual(a, b reflect.Value) bool {
return a.Equal(b)
}

// testStructEquality should NOT be flagged - comparing structs with typed equality
func testStructEquality(a, b struct{ x int }) bool {
return a == b
}

// testNilComparison should NOT be flagged - comparing with nil
func testNilComparison(a interface{}) bool {
return a == nil
}

func testWithNolintDirective(a, b interface{}) bool {
// This should NOT be flagged because of the nolint directive
return reflect.DeepEqual(a, b) //nolint:reflectdeepequalusage
}
2 changes: 2 additions & 0 deletions pkg/linters/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
"github.com/github/gh-aw/pkg/linters/packagelevelmutableslicemap"
panicinlibrarycode "github.com/github/gh-aw/pkg/linters/panic-in-library-code"
"github.com/github/gh-aw/pkg/linters/rawloginlib"
reflectdeepequalusage "github.com/github/gh-aw/pkg/linters/reflect-deepequal-usage"
"github.com/github/gh-aw/pkg/linters/regexpcompileinfunction"
"github.com/github/gh-aw/pkg/linters/regexpdynamicpattern"
"github.com/github/gh-aw/pkg/linters/seenmapbool"
Expand Down Expand Up @@ -133,6 +134,7 @@ var allAnalyzers = []*analysis.Analyzer{
packagelevelmutableslicemap.Analyzer,
panicinlibrarycode.Analyzer,
rawloginlib.Analyzer,
reflectdeepequalusage.Analyzer,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] Running the compiled binary against this repo, reflectdeepequalusage already flags 8 pre-existing reflect.DeepEqual() call sites in pkg/cli and pkg/workflow (e.g. mcp_config_file.go:140, edit_command.go:361/375/384, audit_threat_detection_session.go:91). The PR doesn't add nolint:reflectdeepequalusage to any of them, nor does it add -reflectdeepequalusage to the CI allowlist in .github/workflows/cgo.yml, so the linter is registered but silently inert in CI (the LINTER_FLAGS list acts as an explicit allowlist) while go vet/local make golint-custom runs (which use no LINTER_FLAGS override) would immediately fail on the 8 existing call sites.

💡 What to check

Either:

  1. Add -reflectdeepequalusage to both cgo.yml LINTER_FLAGS lists and fix/nolint the 8 existing call sites, or
  2. If this is intentionally left disabled in CI pending migration, note that explicitly in the PR description so reviewers don't assume it's active.

Right now the PR claims "Successfully integrated with the multichecker" but doesn't clarify it has zero effect in the actual CI gate.

@copilot please address this.

regexpcompileinfunction.Analyzer,
regexpdynamicpattern.Analyzer,
ssljson.Analyzer,
Expand Down
Loading