| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "go/ast" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | func structSource(guard string, scalars int) string { |
| 11 | var b strings.Builder |
| 12 | b.WriteString("package p\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n)\n\nvar _ = sync.Mutex{}\nvar _ = atomic.Bool{}\n\ntype T struct {\n") |
| 13 | if guard != "" { |
| 14 | fmt.Fprintf(&b, "\t%s\n", guard) |
| 15 | } |
| 16 | for i := range scalars { |
| 17 | fmt.Fprintf(&b, "\tf%d bool\n", i) |
| 18 | } |
| 19 | b.WriteString("}\n") |
| 20 | return b.String() |
| 21 | } |
| 22 | |
| 23 | // The rule exists for state several goroutines reach. A record with many fields |
| 24 | // is describing many things, which is not the same defect and would bury it. |
| 25 | func TestStructStateIgnoresTypesWithoutASynchronisationPrimitive(t *testing.T) { |
| 26 | s := parseBytes("t.go", []byte(structSource("", maxScalarFields*4))) |
| 27 | if got := checkStructState(s); len(got) != 0 { |
| 28 | t.Fatalf("a plain record was flagged: %v", got) |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | func TestStructStateFlagsGuardedTypesPastTheCeiling(t *testing.T) { |
| 33 | cases := []struct { |
| 34 | guard string |
| 35 | // An atomic guard is itself one of the scalars it makes concurrent, so |
| 36 | // it counts twice over; a mutex guards other fields and counts as none. |
| 37 | selfCounts int |
| 38 | }{ |
| 39 | {"mu sync.Mutex", 0}, |
| 40 | {"mu sync.RWMutex", 0}, |
| 41 | {"mu *sync.Mutex", 0}, |
| 42 | {"ready atomic.Bool", 1}, |
| 43 | } |
| 44 | for _, tc := range cases { |
| 45 | t.Run(tc.guard, func(t *testing.T) { |
| 46 | s := parseBytes("t.go", []byte(structSource(tc.guard, maxScalarFields+3))) |
| 47 | found := checkStructState(s) |
| 48 | if len(found) != 1 { |
| 49 | t.Fatalf("guarded type past the ceiling produced %d findings, want 1", len(found)) |
| 50 | } |
| 51 | if want := 3 + tc.selfCounts; found[0].Weight != want { |
| 52 | t.Fatalf("weight = %d, want %d: the excess over the ceiling, so a worse struct outranks a better one", |
| 53 | found[0].Weight, want) |
| 54 | } |
| 55 | }) |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | func TestStructStateCountsAtomicsAsScalars(t *testing.T) { |
| 60 | src := "package p\n\nimport \"sync/atomic\"\n\ntype T struct {\n\ta atomic.Bool\n\tb atomic.Int64\n\tc atomic.Uint64\n}\n" |
| 61 | s := parseBytes("t.go", []byte(src)) |
| 62 | if got := scalarFieldCount(firstStruct(t, s)); got != 3 { |
| 63 | t.Fatalf("scalarFieldCount = %d, want 3: an atomic's concurrency contract is per-field", got) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestStructStateCountsEachNameInAGroupedDeclaration(t *testing.T) { |
| 68 | src := "package p\n\nimport \"sync\"\n\ntype T struct {\n\tmu sync.Mutex\n\ta, b, c bool\n}\n" |
| 69 | s := parseBytes("t.go", []byte(src)) |
| 70 | if got := scalarFieldCount(firstStruct(t, s)); got != 3 { |
| 71 | t.Fatalf("scalarFieldCount = %d, want 3: `a, b, c bool` is three independent flags", got) |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | // Grouping by lifetime is the fix the message asks for, so it has to register: |
| 76 | // one sub-state field must replace the whole product it absorbed. |
| 77 | func TestStructStateFallsWhenScalarsMoveIntoASubState(t *testing.T) { |
| 78 | before := parseBytes("t.go", []byte(structSource("mu sync.Mutex", maxScalarFields+5))) |
| 79 | if len(checkStructState(before)) != 1 { |
| 80 | t.Fatal("fixture did not exceed the ceiling") |
| 81 | } |
| 82 | after := parseBytes("t.go", []byte("package p\n\nimport \"sync\"\n\ntype sub struct{ a, b, c, d, e bool }\n\ntype T struct {\n\tmu sync.Mutex\n\tturn sub\n}\n")) |
| 83 | if got := checkStructState(after); len(got) != 0 { |
| 84 | t.Fatalf("grouping five flags into one sub-state still flagged: %v", got) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func TestStructStateSkipsTestFiles(t *testing.T) { |
| 89 | s := parseBytes("t_test.go", []byte(structSource("mu sync.Mutex", maxScalarFields*3))) |
| 90 | if got := checkStructState(s); got != nil { |
| 91 | t.Fatalf("test file measured: %v", got) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func firstStruct(t *testing.T, s *sourceFile) *ast.StructType { |
| 96 | t.Helper() |
| 97 | var out *ast.StructType |
| 98 | ast.Inspect(s.file, func(n ast.Node) bool { |
| 99 | if st, ok := n.(*ast.StructType); ok && out == nil { |
| 100 | out = st |
| 101 | } |
| 102 | return out == nil |
| 103 | }) |
| 104 | if out == nil { |
| 105 | t.Fatal("no struct in source") |
| 106 | } |
| 107 | return out |
| 108 | } |
| 109 |