返回 DeepSeek-Reasonix
structstate_test.go
根目录 / tools / repolint / structstate_test.go
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
109 lines GO