diff --git a/internal/analysis/analysis_test.go b/internal/analysis/analysis_test.go index 85b2719..e6708ef 100644 --- a/internal/analysis/analysis_test.go +++ b/internal/analysis/analysis_test.go @@ -697,8 +697,11 @@ func TestP1_CustomResponseWriter(t *testing.T) { func TestP1_MapMutation(t *testing.T) { result := analyzeFunc(t, "p1effects", "WriteToMap") - if !hasEffect(result.SideEffects, taxonomy.MapMutation) { - t.Error("expected MapMutation for WriteToMap") + if count := countEffects(result.SideEffects, taxonomy.MapMutation); count != 1 { + t.Fatalf("expected exactly 1 MapMutation for WriteToMap, got %d", count) + } + if e := effectWithTarget(result.SideEffects, taxonomy.MapMutation, "m"); e == nil { + t.Error("expected MapMutation with target \"m\"") } } @@ -713,8 +716,11 @@ func TestP1_MapMutation_ReadOnly(t *testing.T) { func TestP1_SliceMutation(t *testing.T) { result := analyzeFunc(t, "p1effects", "WriteToSlice") - if !hasEffect(result.SideEffects, taxonomy.SliceMutation) { - t.Error("expected SliceMutation for WriteToSlice") + if count := countEffects(result.SideEffects, taxonomy.SliceMutation); count != 1 { + t.Fatalf("expected exactly 1 SliceMutation for WriteToSlice, got %d", count) + } + if e := effectWithTarget(result.SideEffects, taxonomy.SliceMutation, "s"); e == nil { + t.Error("expected SliceMutation with target \"s\"") } } diff --git a/internal/analysis/p1effects_test.go b/internal/analysis/p1effects_test.go index 86db8da..e9cb103 100644 --- a/internal/analysis/p1effects_test.go +++ b/internal/analysis/p1effects_test.go @@ -146,7 +146,8 @@ func TestAnalyzeP1Effects_Direct_HTTPResponseWrite(t *testing.T) { } // TestAnalyzeP1Effects_Direct_MapMutation verifies that AnalyzeP1Effects -// detects MapMutation for a function that assigns to a map index. +// detects exactly one MapMutation with the correct target for a function +// that assigns to a map index parameter. func TestAnalyzeP1Effects_Direct_MapMutation(t *testing.T) { pkg := loadTestPackage(t, "p1effects") fd := analysis.FindFuncDecl(pkg, "WriteToMap") @@ -156,23 +157,24 @@ func TestAnalyzeP1Effects_Direct_MapMutation(t *testing.T) { effects := analysis.AnalyzeP1Effects(pkg.Fset, pkg.TypesInfo, fd, pkg.PkgPath, "WriteToMap") - if !hasEffect(effects, taxonomy.MapMutation) { - t.Error("expected MapMutation effect for WriteToMap") + if count := countEffects(effects, taxonomy.MapMutation); count != 1 { + t.Fatalf("expected exactly 1 MapMutation, got %d", count) } - for _, e := range effects { - if e.Type == taxonomy.MapMutation { - if e.Tier != taxonomy.TierP1 { - t.Errorf("MapMutation tier: got %s, want P1", e.Tier) - } - if e.Description == "" { - t.Error("MapMutation description must not be empty") - } - } + e := effectWithTarget(effects, taxonomy.MapMutation, "m") + if e == nil { + t.Fatal("expected MapMutation with target \"m\"") + } + if e.Tier != taxonomy.TierP1 { + t.Errorf("MapMutation tier: got %s, want P1", e.Tier) + } + if e.Description == "" { + t.Error("MapMutation description must not be empty") } } // TestAnalyzeP1Effects_Direct_SliceMutation verifies that AnalyzeP1Effects -// detects SliceMutation for a function that assigns to a slice index. +// detects exactly one SliceMutation with the correct target for a function +// that assigns to a slice index parameter. func TestAnalyzeP1Effects_Direct_SliceMutation(t *testing.T) { pkg := loadTestPackage(t, "p1effects") fd := analysis.FindFuncDecl(pkg, "WriteToSlice") @@ -182,18 +184,18 @@ func TestAnalyzeP1Effects_Direct_SliceMutation(t *testing.T) { effects := analysis.AnalyzeP1Effects(pkg.Fset, pkg.TypesInfo, fd, pkg.PkgPath, "WriteToSlice") - if !hasEffect(effects, taxonomy.SliceMutation) { - t.Error("expected SliceMutation effect for WriteToSlice") + if count := countEffects(effects, taxonomy.SliceMutation); count != 1 { + t.Fatalf("expected exactly 1 SliceMutation, got %d", count) } - for _, e := range effects { - if e.Type == taxonomy.SliceMutation { - if e.Tier != taxonomy.TierP1 { - t.Errorf("SliceMutation tier: got %s, want P1", e.Tier) - } - if e.Description == "" { - t.Error("SliceMutation description must not be empty") - } - } + e := effectWithTarget(effects, taxonomy.SliceMutation, "s") + if e == nil { + t.Fatal("expected SliceMutation with target \"s\"") + } + if e.Tier != taxonomy.TierP1 { + t.Errorf("SliceMutation tier: got %s, want P1", e.Tier) + } + if e.Description == "" { + t.Error("SliceMutation description must not be empty") } } @@ -444,3 +446,46 @@ func TestAnalyzeP1Effects_Direct_WriteToStructMap(t *testing.T) { } } } + +// TestAnalyzeP1Effects_PartialResponseWriter verifies that a type with +// Write + Header but NOT WriteHeader produces WriterOutput (it satisfies +// io.Writer) but does NOT produce HTTPResponseWrite. +func TestAnalyzeP1Effects_PartialResponseWriter(t *testing.T) { + pkg := loadTestPackage(t, "p1effects") + fd := analysis.FindFuncDecl(pkg, "HandlePartialRW") + if fd == nil { + t.Fatal("HandlePartialRW not found in p1effects package") + } + + effects := analysis.AnalyzeP1Effects(pkg.Fset, pkg.TypesInfo, fd, pkg.PkgPath, "HandlePartialRW") + + if hasEffect(effects, taxonomy.HTTPResponseWrite) { + t.Error("PartialResponseWriter (missing WriteHeader) must not produce HTTPResponseWrite") + } + if !hasEffect(effects, taxonomy.WriterOutput) { + t.Error("PartialResponseWriter has valid Write([]byte)(int,error) — expected WriterOutput") + } +} + +// TestAnalyzeP1Effects_WrongWriteReturn verifies that a type whose Write +// method returns only int (not (int, error)) is not classified as either +// io.Writer or http.ResponseWriter. +func TestAnalyzeP1Effects_WrongWriteReturn(t *testing.T) { + pkg := loadTestPackage(t, "p1effects") + fd := analysis.FindFuncDecl(pkg, "HandleWrongWriteReturn") + if fd == nil { + t.Fatal("HandleWrongWriteReturn not found in p1effects package") + } + + effects := analysis.AnalyzeP1Effects(pkg.Fset, pkg.TypesInfo, fd, pkg.PkgPath, "HandleWrongWriteReturn") + + if len(effects) != 0 { + t.Errorf("expected no effects for WrongWriteReturn, got %d: %v", len(effects), effects) + } + if hasEffect(effects, taxonomy.HTTPResponseWrite) { + t.Error("WrongWriteReturn must not produce HTTPResponseWrite — Write signature mismatch") + } + if hasEffect(effects, taxonomy.WriterOutput) { + t.Error("WrongWriteReturn must not produce WriterOutput — Write([]byte) int is not io.Writer") + } +} diff --git a/internal/analysis/testdata/src/p1effects/p1effects.go b/internal/analysis/testdata/src/p1effects/p1effects.go index 9aeadfe..c4a8e0c 100644 --- a/internal/analysis/testdata/src/p1effects/p1effects.go +++ b/internal/analysis/testdata/src/p1effects/p1effects.go @@ -193,6 +193,35 @@ func HandleCustomRW(w *CustomResponseWriter) { w.Write([]byte("ok")) } +// --- Partial http.ResponseWriter (issue #162) --- + +// PartialResponseWriter has Write + Header but NOT WriteHeader. +// Should produce WriterOutput, NOT HTTPResponseWrite. +type PartialResponseWriter struct{} + +func (p *PartialResponseWriter) Header() http.Header { return nil } +func (p *PartialResponseWriter) Write(b []byte) (int, error) { return len(b), nil } + +// HandlePartialRW calls Write on a type with only 2 of 3 ResponseWriter methods. +func HandlePartialRW(w *PartialResponseWriter) { + w.Write([]byte("ok")) +} + +// --- Near-miss Write signature (issue #162) --- + +// WrongWriteReturn has Write([]byte) int — missing error return. +type WrongWriteReturn struct{} + +func (wr *WrongWriteReturn) Header() http.Header { return nil } +func (wr *WrongWriteReturn) Write(b []byte) int { return len(b) } +func (wr *WrongWriteReturn) WriteHeader(statusCode int) {} + +// HandleWrongWriteReturn calls Write on a type whose Write signature +// doesn't match io.Writer. Should NOT produce HTTPResponseWrite or WriterOutput. +func HandleWrongWriteReturn(w *WrongWriteReturn) { + w.Write([]byte("ok")) +} + // --- Pure function (no P1 effects) --- // PureP1 has no P1 side effects.