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
14 changes: 10 additions & 4 deletions internal/analysis/analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\"")
}
}

Expand All @@ -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\"")
}
}

Expand Down
93 changes: 69 additions & 24 deletions internal/analysis/p1effects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
}
}

Expand Down Expand Up @@ -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")
}
}
29 changes: 29 additions & 0 deletions internal/analysis/testdata/src/p1effects/p1effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading