返回 DeepSeek-Reasonix
structstate.go
根目录 / tools / repolint / structstate.go
1 package main
2
3 import (
4 "fmt"
5 "go/ast"
6 )
7
8 // Each independent scalar multiplies the states a guarded type can be in, and
9 // nothing records which combinations are legal; grouping them by lifetime costs
10 // one field and removes the whole product. Only types owning a mutex or atomic
11 // are counted — a struct without one is not concurrently mutated state, and
12 // counting config or DTOs would bury this finding under a translation table.
13 const maxScalarFields = 12
14
15 var scalarBasicTypes = map[string]bool{
16 "bool": true, "string": true, "byte": true, "rune": true,
17 "int": true, "int8": true, "int16": true, "int32": true, "int64": true,
18 "uint": true, "uint8": true, "uint16": true, "uint32": true, "uint64": true,
19 "uintptr": true, "float32": true, "float64": true,
20 }
21
22 // atomic.Bool and friends are scalars whose concurrency contract is per-field,
23 // which is exactly the combination problem this counts.
24 var scalarQualifiedTypes = map[string]bool{
25 "atomic.Bool": true, "atomic.Int32": true, "atomic.Int64": true,
26 "atomic.Uint32": true, "atomic.Uint64": true, "atomic.Pointer": true,
27 "atomic.Value": true, "time.Duration": true, "time.Time": true,
28 }
29
30 // guardsConcurrentState reports whether the type owns a synchronisation
31 // primitive, which is what separates state several goroutines reach from a
32 // record that merely has fields.
33 func guardsConcurrentState(st *ast.StructType) bool {
34 if st.Fields == nil {
35 return false
36 }
37 for _, field := range st.Fields.List {
38 sel, ok := unwrapPointer(field.Type).(*ast.SelectorExpr)
39 if !ok {
40 continue
41 }
42 pkg, ok := sel.X.(*ast.Ident)
43 if !ok {
44 continue
45 }
46 switch pkg.Name + "." + sel.Sel.Name {
47 case "sync.Mutex", "sync.RWMutex":
48 return true
49 }
50 if pkg.Name == "atomic" {
51 return true
52 }
53 }
54 return false
55 }
56
57 func unwrapPointer(expr ast.Expr) ast.Expr {
58 if star, ok := expr.(*ast.StarExpr); ok {
59 return star.X
60 }
61 return expr
62 }
63
64 func scalarFieldCount(st *ast.StructType) int {
65 if st.Fields == nil {
66 return 0
67 }
68 total := 0
69 for _, field := range st.Fields.List {
70 if !isScalarType(field.Type) {
71 continue
72 }
73 // An embedded scalar still occupies one slot in the product.
74 total += max(len(field.Names), 1)
75 }
76 return total
77 }
78
79 func isScalarType(expr ast.Expr) bool {
80 switch t := expr.(type) {
81 case *ast.Ident:
82 return scalarBasicTypes[t.Name]
83 case *ast.SelectorExpr:
84 pkg, ok := t.X.(*ast.Ident)
85 if !ok {
86 return false
87 }
88 return scalarQualifiedTypes[pkg.Name+"."+t.Sel.Name]
89 case *ast.IndexExpr: // atomic.Pointer[T]
90 return isScalarType(t.X)
91 }
92 return false
93 }
94
95 func checkStructState(s *sourceFile) []Finding {
96 if s.isTest() {
97 return nil
98 }
99 var out []Finding
100 ast.Inspect(s.file, func(n ast.Node) bool {
101 spec, ok := n.(*ast.TypeSpec)
102 if !ok {
103 return true
104 }
105 st, ok := spec.Type.(*ast.StructType)
106 if !ok {
107 return true
108 }
109 if !guardsConcurrentState(st) {
110 return false
111 }
112 if n := scalarFieldCount(st); n > maxScalarFields {
113 out = append(out, Finding{s.rel, s.line(spec.Pos()), ruleStructState,
114 fmt.Sprintf("%s carries %d scalar state fields, over the %d ceiling; group them by lifetime",
115 spec.Name.Name, n, maxScalarFields),
116 n - maxScalarFields})
117 }
118 return false
119 })
120 return out
121 }
122
122 lines GO