//go:build ignore
// 命令 audit_parallel 审计插件工具的并发安全性。
//
// 存在理由:正则扫 ToolDef 字面量**不可靠** —— 我用它审计主仓 27 个工具时,
// 把 config_set 判成"无共享写",而它的 handler 其实在 p.handleSet 里且无锁。
// 原因:RegisterTool 的第三个参数是方法名/闭包,正则看不到执行体。
//
// 本工具用 go/ast 跟进**注册点到 handler 实现**,对每个工具判定:
// - 找到所有可达的写操作(字段赋值、append、map 写入、文件写、exec、SDK Set)
// - 找到所有互斥保护(Lock/RLock 覆盖该写,还是写发生在锁外)
//
// 输出三态:SAFE(只读)/ SERIAL(有写或顺序约束)/ UNKNOWN(追不到实现)。
// UNKNOWN 一律不标 ParallelSafe —— 追不到就不能声称安全。
//
// 用法:go run ./tools/audit_parallel.go
...
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
)
// exprString 把表达式打印回源码文本。
func exprString(fset *token.FileSet, e ast.Expr) string {
var sb strings.Builder
if err := printer.Fprint(&sb, fset, e); err != nil {
return ""
}
return sb.String()
}
// verdict 是单个工具的审计结论。
type verdict string
const (
verdictSafe verdict = "SAFE" // 只读,可并发
verdictSerial verdict = "SERIAL" // 有写或顺序约束,必须串行
verdictUnknown verdict = "UNKNOWN" // 追不到实现,不声明
)
// finding 是审计中命中的一个风险点。
type finding struct {
line int
what string
}
// toolAudit 是一个工具的审计结果。
type toolAudit struct {
name string
file string
verdict verdict
findings []finding
// implFile/implLine 指向追到的 handler 实现位置
implFile string
implLine int
}
func main() {
dirs := os.Args[1:]
if len(dirs) == 0 {
fmt.Fprintln(os.Stderr, "用法: audit_parallel ...")
os.Exit(2)
}
all := map[string]*toolAudit{}
for _, dir := range dirs {
auditDir(dir, all)
}
report(all)
}
func auditDir(dir string, out map[string]*toolAudit) {
// 收集该目录下所有 .go 源码(用于跨文件跟进方法体)
files := map[string]*ast.File{}
fset := token.NewFileSet()
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
return nil
}
if strings.HasSuffix(path, "_test.go") {
return nil
}
f, perr := parser.ParseFile(fset, path, nil, 0)
if perr != nil {
return nil
}
files[path] = f
return nil
})
// 索引:方法名 -> 实现((file, receiver, method))
methods := indexMethods(fset, files)
// 索引:**变量名** -> 可能的接收者类型
//
// 为何需要:调用点写的是 p.handleRead(),定义处是 func (p *Plugin) handleRead()。
// 变量名 p 与类型名 Plugin 是两个名字空间,必须显式连起来。
recvTypes := indexRecvTypes(fset, files)
for path, f := range files {
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok || !isRegisterTool(call) {
return true
}
name := toolNameOf(call, fset)
if name == "" {
return true
}
ta := &toolAudit{name: name, file: path, verdict: verdictUnknown}
// 作者已声明的**优先**,且不被后续判定覆盖。
//
// ⚠️ 我第一版把"已声明"当作一条 finding 记录,然后照常跑
// 写入检测并**覆盖** verdict —— 结果 plugin_install(已标
// Serial:true,实现里 exec.Command 也确实在跑)被判成 SAFE。
// 审计工具自己给出与代码相反的结论,比没有工具更糟。
if decl := declaredFlags(call); decl != "" {
// 映射到三态之一。
//
// ⚠️ 我第一版直接 verdict = decl(值是 "Serial:true"),
// 而 report 只认 SAFE/SERIAL/UNKNOWN 三个枚举值 ⇒ 全部落进
// UNKNOWN 分支,输出"共 13:UNKNOWN 13",看着像工具没在工作。
// 一枚举值被字符串污染时,症状离原因很远。
if strings.Contains(decl, "Serial") {
ta.verdict = verdictSerial
} else {
ta.verdict = verdictSafe
}
ta.findings = append(ta.findings, finding{0, "已声明 " + decl + "(作者判断,采信)"})
out[name] = ta
return true
}
// 跟进 handler 实现
impl := handlerImplOf(call, fset, methods, recvTypes)
if impl == nil {
ta.findings = append(ta.findings, finding{0, "追不到 handler 实现(保守:不声明并发安全) diag=" + diagOf(call)})
} else {
ta.implFile, ta.implLine = impl.file, impl.line
// ★ 递归跟进:写操作常常**不在** handler 体内。
// 手工核实 plugin_install 时发现,闭包体只写了两行
// `return p.installFromPath(...)`,真正的写在那个方法里 ——
// 只扫一层会把它判成 SAFE(第一版就是这个 bug)。
fs := scanWritesDeep(impl, methods, map[string]bool{}, 0)
ta.findings = append(ta.findings, fs...)
hasWrite := false
for _, x := range fs {
if isWriteKind(x.what) {
hasWrite = true
}
}
if hasWrite {
ta.verdict = verdictSerial
} else {
ta.verdict = verdictSafe
}
}
if old, exists := out[name]; !exists || old.verdict == verdictUnknown {
out[name] = ta
}
return true
})
}
}
// indexRecvTypes 建立 "变量名 -> 接收者类型名列表" 的索引。
func indexRecvTypes(fset *token.FileSet, files map[string]*ast.File) map[string][]string {
out := map[string][]string{}
for _, f := range files {
// ★ 方法接收者变量名 → 接收者类型。
//
// example/files 里根本没有 `p := &Plugin{}` 这类语句 —— p 是
// Start 的方法接收者。于是局部变量索引全空,143 个工具跟着全 UNKNOWN。
// 症状("追不到实现")完全看不出真因是"p 从来不是局部变量"。
for _, d := range f.Decls {
fd, ok := d.(*ast.FuncDecl)
if !ok || fd.Recv == nil || len(fd.Recv.List) == 0 {
continue
}
var recvType string
if st, ok := fd.Recv.List[0].Type.(*ast.StarExpr); ok {
if id, ok := st.X.(*ast.Ident); ok {
recvType = id.Name
}
} else if id, ok := fd.Recv.List[0].Type.(*ast.Ident); ok {
recvType = id.Name
}
if recvType == "" {
continue
}
for _, nm := range fd.Recv.List[0].Names {
out[nm.Name] = appendUnique(out[nm.Name], recvType)
}
}
// 局部变量 p := &Plugin{} / var p *Plugin / p := New(...)
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.AssignStmt:
if len(x.Lhs) == 1 && len(x.Rhs) == 1 {
if id, ok := x.Lhs[0].(*ast.Ident); ok {
for _, t := range typeNamesOf(x.Rhs[0]) {
out[id.Name] = appendUnique(out[id.Name], t)
}
}
}
case *ast.ValueSpec:
// ⚠️ ValueSpec.Type 是 ast.Expr(**单个**类型表达式),
// 不是切片 —— 我按 FieldList 的形状写了 x.Type[i],编译不过。
// 有显式类型才建索引;`p := New(...)` 走上面 AssignStmt 分支。
if x.Type != nil {
for _, id := range x.Names {
for _, t := range typeNamesOf(x.Type) {
out[id.Name] = appendUnique(out[id.Name], t)
}
}
}
}
return true
})
// 结构体字段 p.sdk / p.xxx 指向含 Plugin 的类型:并入该结构体名
for _, d := range f.Decls {
gd, ok := d.(*ast.GenDecl)
if !ok || gd.Tok != token.TYPE {
continue
}
for _, sp := range gd.Specs {
ts, ok := sp.(*ast.TypeSpec)
if !ok {
continue
}
st, ok := ts.Type.(*ast.StructType)
if !ok {
continue
}
for _, fld := range st.Fields.List {
for _, t := range typeNamesOf(fld.Type) {
out[ts.Name.Name] = appendUnique(out[ts.Name.Name], t)
}
}
}
}
}
return out
}
func appendUnique(xs []string, x string) []string {
for _, y := range xs {
if y == x {
return xs
}
}
return append(xs, x)
}
// typeNamesOf 从表达式/类型里抽出可能的类型名(含 New(T{}) 这类构造)。
func typeNamesOf(e ast.Expr) []string {
var out []string
switch x := e.(type) {
case *ast.StarExpr:
return typeNamesOf(x.X)
case *ast.Ident:
return []string{x.Name}
case *ast.SelectorExpr:
return []string{x.Sel.Name}
case *ast.CompositeLit:
return typeNamesOf(x.Type)
case *ast.CallExpr:
// New(Plugin{}) → Plugin;也支持 f(&Plugin{}) / NewPlugin()
if id, ok := x.Fun.(*ast.Ident); ok && id.Name == "New" && len(x.Args) > 0 {
return typeNamesOf(x.Args[0])
}
if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
return []string{strings.TrimPrefix(sel.Sel.Name, "New")}
}
}
return out
}
type methodImpl struct {
file string
line int
body *ast.BlockStmt
recv string // 接收者类型名(大写化),如 "Plugin"
// locked 记录是否在函数内出现 Lock/RLock
locked bool
// selfCall 记录本方法内调用的 p.xxx(...) 形式,便于继续跟进
selfCalls []string
}
func indexMethods(fset *token.FileSet, files map[string]*ast.File) map[string]*methodImpl {
out := map[string]*methodImpl{}
for path, f := range files {
for _, d := range f.Decls {
fd, ok := d.(*ast.FuncDecl)
if !ok || fd.Body == nil {
continue
}
name := fd.Name.Name
recv := ""
if fd.Recv != nil && len(fd.Recv.List) > 0 {
rt := fd.Recv.List[0].Type
if t, ok := rt.(*ast.StarExpr); ok {
if id, ok := t.X.(*ast.Ident); ok {
recv = id.Name
}
} else if id, ok := rt.(*ast.Ident); ok {
recv = id.Name
}
if recv != "" {
name = "*" + recv + "." + name
}
}
// ★ recv 与 selfCalls 都必须填:scanWritesDeep 用 recv 组候选键去
// 跟进下层方法,用 selfCalls 才知道要跟进谁。缺任何一个,
// 递归跟进就是空转 —— handlerRestart → p.stopServer/startServer
// 这类"只调两个方法"的写法会静默漏判成只读。
mi := &methodImpl{
file: path,
line: fset.Position(fd.Pos()).Line,
body: fd.Body,
recv: recv,
locked: hasMutexIn(fd.Body),
selfCalls: selfCallsOf(fd.Body),
}
out[name] = mi
if recv != "" {
out[recv+"."+fd.Name.Name] = mi
}
}
}
return out
}
// handlerImplOf 从 RegisterTool 的第三个参数跟到实现。
func handlerImplOf(call *ast.CallExpr, fset *token.FileSet, methods map[string]*methodImpl, recvTypes map[string][]string) *methodImpl {
if len(call.Args) < 3 {
return nil
}
switch a := call.Args[2].(type) {
case *ast.FuncLit: // 闭包
// ⚠️ recv 留空是有代价的:scanWritesDeep 跟进 selfCalls 时用
// "*"+recv+"."+name 找方法实现,recv 为空就永远找不到 ⇒
// 闭包里对 p.xxx(...) 的调用全部追不下去,write 被静默漏掉。
// 症状是"看起来有扫描、实际漏判"。
return &methodImpl{file: "", line: fset.Position(a.Pos()).Line, body: a.Body, locked: hasMutexIn(a.Body), selfCalls: selfCallsOf(a.Body)}
case *ast.SelectorExpr: // p.handleRead
//
// ★ 这里踩过一个隐蔽的坑:调用点的接收者是**变量名**(p),
// 而方法定义的接收者是**类型名**(Plugin)。我第一版拿变量名
// 直接去查类型索引,**永远匹配不上**,于是 143 个工具全报 UNKNOWN ——
// 症状(全是未知)完全看不出是名字空间搞错了。
// 修法:用变量名 + 方法名,跨 recvTypes 找出所有可能的类型。
if recv, ok := a.X.(*ast.Ident); ok {
if recvTypes != nil {
if types, ok := recvTypes[recv.Name]; ok {
for _, rt := range types {
for _, k := range []string{"*" + rt + "." + a.Sel.Name, rt + "." + a.Sel.Name} {
if m, ok := methods[k]; ok {
return m
}
}
}
}
}
// 退化:直接按名字试(无类型信息时)
if m, ok := methods["*"+recv.Name+"."+a.Sel.Name]; ok {
return m
}
if m, ok := methods[a.Sel.Name]; ok {
return m
}
}
case *ast.Ident: // 已声明的 handler 变量
key := a.Name
if m, ok := methods[key]; ok {
return m
}
}
return nil
}
func toolNameOf(call *ast.CallExpr, fset *token.FileSet) string {
if len(call.Args) == 0 {
return ""
}
switch a := call.Args[0].(type) {
case *ast.BasicLit:
return strings.Trim(a.Value, `"`)
case *ast.BinaryExpr: // tp+"read" 之类
//
// ⚠️ 我第一版用 fmt.Sprintf("%v", a.Y) 拼名字,而 a.Y 是 *ast.BasicLit,
// %v 打印的是 token 内部结构 —— 输出长这样:
// tp&{10500 10515 STRING "manage_social"}
// 名字里混着指针地址,人根本没法核对。必须取 BasicLit.Value。
lhs := exprString(fset, a.X)
rhs := ""
if lit, ok := a.Y.(*ast.BasicLit); ok {
rhs = strings.Trim(lit.Value, `"`)
}
// 归一化:变量前缀(p.name / tp / p.tp)在运行期才确定,
// 静态只保留字面量那一段,前缀分隔符一并去掉。
if _, isIdent := a.X.(*ast.Ident); isIdent {
return rhs
}
if i := strings.Index(lhs, "."); i >= 0 {
lhs = lhs[i+1:]
}
return lhs + rhs
case *ast.Ident:
return a.Name
case *ast.SelectorExpr:
return a.Sel.Name
}
return ""
}
func isRegisterTool(call *ast.CallExpr) bool {
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return false
}
switch sel.Sel.Name {
case "RegisterTool":
return true
}
// mockSDK / shim.RegisterTool 之类
if strings.Contains(sel.Sel.Name, "RegisterTool") {
return true
}
return false
}
func declaredFlags(call *ast.CallExpr) string {
var sb strings.Builder
ast.Inspect(call, func(n ast.Node) bool {
kv, ok := n.(*ast.KeyValueExpr)
if !ok {
return true
}
k, ok1 := kv.Key.(*ast.Ident)
v, ok2 := kv.Value.(*ast.Ident)
if ok1 && ok2 && (k.Name == "ParallelSafe" || k.Name == "Serial") {
fmt.Fprintf(&sb, "%s:%s ", k.Name, v.Name)
}
return true
})
return strings.TrimSpace(sb.String())
}
// writePatterns 是需要视为"写"的调用与赋值形态。
// httpWriteVerbs 是"写"语义的 HTTP 动词。
//
// ★ 这条是被手工核对逼出来的:vanblog 的 manage_social / manage_settings
// 全走 `p.do("GET"|"POST"|"PUT"|..., ...)` 这个通用包装器,方法名里没有
// 任何 write/delete 迹象,纯靠方法名匹配**全部漏判成 SAFE** ——
// 而它们明确含 POST/PUT,是写操作。工具给出与代码相反的结论,
// 比不给结论更危险(人会信它)。
var httpWriteVerbs = []string{"POST", "PUT", "PATCH", "DELETE"}
// httpReadVerbs 只读的动词(用于在"混合"工具上区分)。
var httpReadVerbs = []string{"GET", "HEAD", "OPTIONS"}
var writePatterns = []struct{ what, pat string }{
{"http-write-verb", ""}, // 占位:走专门的检查
{"exec", "exec.Command"},
{"exec", "exec.CommandContext"},
{"file-write", "os.WriteFile"},
{"file-remove", "os.Remove"},
{"file-remove", "os.RemoveAll"},
{"file-rename", "os.Rename"},
{"mkdir", "os.Mkdir"},
{"mkdir", "os.MkdirAll"},
{"http-post", "http.Post"},
{"http-do", "client.Do"},
// ⚠️ 这些模式**不带括号**:callString 收集的是链上方法名并用点连起来
// ("Settings.Set"),不是完整调用文本。我第一版写成 ".Set(",
// 于是永远匹配不上 —— handleConfigure(写配置 + 启停服务)被判只读。
// 症状是"漏判",比误判更难发现:结果看起来仍然合理。
{"sdk-set", ".Set"},
{"sdk-save", ".Save"},
{"sdk-update", ".Update"},
{"sdk-delete", ".Delete"},
{"sdk-add", ".Add"},
{"sdk-install", ".Install"},
{"sdk-restart", ".Restart"},
{"sdk-shutdown", ".Shutdown"},
{"write", ".Write"},
{"start", ".Start"},
{"stop", ".Stop"},
{"kill", ".Kill"},
{"os-write", "os.WriteFile"},
{"os-remove", "os.Remove"},
{"exec", "exec.Command"},
}
func scanWrites(body *ast.BlockStmt) []finding {
var out []finding
// 字段赋值
ast.Inspect(body, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.AssignStmt:
for _, lhs := range x.Lhs {
if isSelector(lhs) {
out = append(out, finding{0, "assign"})
}
}
case *ast.IncDecStmt:
if isSelector(x.X) {
out = append(out, finding{0, "incdec"})
}
case *ast.CallExpr:
s := callString(x)
for _, p := range writePatterns {
if p.pat == "" {
continue
}
if strings.Contains(s, p.pat) {
out = append(out, finding{0, p.what})
}
}
// HTTP 动词检查(覆盖通用包装器)
if v := httpVerbOf(x); v != "" {
for _, w := range httpWriteVerbs {
if v == w {
out = append(out, finding{0, "http-" + v})
break
}
}
}
}
return true
})
// append
ast.Inspect(body, func(n ast.Node) bool {
if c, ok := n.(*ast.CallExpr); ok {
if id, ok := c.Fun.(*ast.Ident); ok && id.Name == "append" {
out = append(out, finding{0, "append"})
}
}
return true
})
// 去重
seen := map[string]bool{}
var uniq []finding
for _, f := range out {
if seen[f.what] {
continue
}
seen[f.what] = true
uniq = append(uniq, f)
}
return uniq
}
// maxFollowDepth 是跟进深度的上界。
//
// 定 3 层不是随便取的:example 插件里最深的链是
// handler → 业务方法 → 存储/请求方法,三层已覆盖;再深的链说明该工具
// 复杂到**人工读**比自动判更可靠,达到上界时按 UNKNOWN 处理(不标并发安全)。
const maxFollowDepth = 3
// scanWritesDeep 递归跟进方法调用,收集写操作。
func scanWritesDeep(impl *methodImpl, methods map[string]*methodImpl, seen map[string]bool, depth int) []finding {
if impl == nil {
return nil
}
key := fmt.Sprintf("%s:%d", impl.file, impl.line)
if seen[key] {
return nil // 环
}
seen[key] = true
defer delete(seen, key)
var out []finding
out = append(out, scanWrites(impl.body)...)
if depth >= maxFollowDepth {
// 到达上界:还有未跟进的自调用就不能声称只读
if len(impl.selfCalls) > 0 {
out = append(out, finding{0, fmt.Sprintf("跟进到深度上界 %d,仍有 %d 个下层调用未展开", maxFollowDepth, len(impl.selfCalls))})
}
return out
}
// 跟进本方法的 selfCalls
for _, c := range impl.selfCalls {
// 在同一接收者类型下找;找不到就跨接收者找
cands := []string{"*" + impl.recv + "." + c, impl.recv + "." + c, c}
found := false
for _, k := range cands {
if impl.recv == "" && k == "*."+c {
continue
}
if m, ok := methods[k]; ok && m != impl {
out = append(out, scanWritesDeep(m, methods, seen, depth+1)...)
found = true
break
}
}
if !found && looksLikeExternal(c) {
out = append(out, finding{0, "外部调用 " + c + "()(可能写,需人工确认)"})
}
}
return dedupeFindings(out)
}
// diagOf 输出第三个参数的形态,用于诊断 handlerImplOf 为何失败。
func diagOf(call *ast.CallExpr) string {
if len(call.Args) < 3 {
return fmt.Sprintf("args=%d", len(call.Args))
}
switch a := call.Args[2].(type) {
case *ast.FuncLit:
return "closure"
case *ast.SelectorExpr:
return "selector:" + selReceiver(a) + "." + a.Sel.Name
case *ast.Ident:
return "ident:" + a.Name
}
return fmt.Sprintf("%T", call.Args[2])
}
// knownReadOnlyCalls 是**确认无副作用**的调用/构造器。
//
// ⚠️ 这张表必须显式列出,不能靠"名字不像写操作"来猜:我第一版用
// looksLikeExternal(黑名单),结果 Marshal / ReadAll / NewRequest /
// NewReader 这些**纯读**的标准库调用全被判"可能写" ⇒ 只读的
// get_article 变成 SERIAL,120 个工具里 119 个被判串行 ——
// 等于工具没在工作,却看上去在工作(保守方向不会引起怀疑)。
//
// 判定原则:**默认怀疑,明确信任**。写不动的东西要逐个列出来。
var knownReadOnlyCalls = map[string]bool{
// 格式化
"Sprintf": true, "Fprintf": true, "Errorf": true, "Fatalf": true,
"Printf": true, "Sprintln": true, "Sprint": true, "Sscanf": true,
// 字符串(纯函数)
"String": true, "TrimSpace": true, "Trim": true, "TrimPrefix": true,
"TrimSuffix": true, "Split": true, "SplitN": true, "Join": true,
"Replace": true, "ReplaceAll": true, "ToLower": true, "ToUpper": true,
"Contains": true, "HasPrefix": true, "HasSuffix": true, "Fields": true,
"Repeat": true, "EqualFold": true, "Title": true,
// 数值
"Min": true, "Max": true, "Abs": true, "Round": true, "Floor": true, "Ceil": true,
// 编解码(纯变换)
"Marshal": true, "Unmarshal": true, "NewDecoder": true, "NewEncoder": true,
// JSON 读取(只读文件/流,不写)
"NewReader": true, "ReadAll": true, "Read": true, "Decode": true,
// HTTP 只读侧
"NewRequest": true, "NewRequestWithContext": true, "Parse": true, "ParseForm": true,
// 时间
"Now": true, "Unix": true, "ParseDuration": true, "After": true,
// 容器
"New": true, "NewMap": true, "Keys": true, "Values": true,
"Len": true, "Cap": true, "Copy": true, "Append": true,
// 容器查
"Get": true, "Load": true, "Exists": true, "List": true, "Query": true,
// 文件只读
"Stat": true, "ReadDir": true, "ReadFile": true, "Glob": true, "Walk": true,
// 错误
"Is": true, "As": true, "Unwrap": true, "Error": true,
}
func looksLikeExternal(name string) bool {
if knownReadOnlyCalls[name] {
return false
}
// 指针/包前缀形式(strings.TrimSpace)取最后一段
if i := strings.LastIndex(name, "."); i >= 0 {
if knownReadOnlyCalls[name[i+1:]] {
return false
}
}
return true
}
func dedupeFindings(in []finding) []finding {
seen := map[string]bool{}
var out []finding
for _, f := range in {
if seen[f.what] {
continue
}
seen[f.what] = true
out = append(out, f)
}
return out
}
func isWriteKind(w string) bool {
switch w {
case "assign", "incdec", "append":
return true
}
return true // 保守:所有命中都算写
}
// httpVerbOf 从调用实参里取 HTTP 动词(如 p.do("POST", ...))。
func httpVerbOf(c *ast.CallExpr) string {
for _, a := range c.Args {
if lit, ok := a.(*ast.BasicLit); ok && lit.Kind == token.STRING {
v := strings.ToUpper(strings.Trim(lit.Value, `"`))
for _, x := range append(append([]string{}, httpWriteVerbs...), httpReadVerbs...) {
if v == x {
return v
}
}
}
}
return ""
}
func isSelector(e ast.Expr) bool { _, ok := e.(*ast.SelectorExpr); return ok }
// callString 收集整条方法链上的**所有**方法名。
//
// ★ 误判根因:`p.sdk.Settings().Set("listen", ...)` 是三段链式调用,
//
// 只看最外层只会得到 "Settings",于是 .Set 这个写操作**整个丢失**,
// handleConfigure(写配置 + 启停服务)被判成只读。
// 链式调用在 Go 里极常见,只看最外层等于漏掉大半写操作。
func callString(c *ast.CallExpr) string {
parts := callChain(c)
return strings.Join(parts, ".")
}
// callChain 自内向外收集链上的方法名/标识符。
func callChain(c *ast.CallExpr) []string {
switch f := c.Fun.(type) {
case *ast.SelectorExpr:
if inner, ok := f.X.(*ast.CallExpr); ok {
return append(callChain(inner), f.Sel.Name)
}
return []string{f.Sel.Name}
case *ast.Ident:
return []string{f.Name}
}
return nil
}
func selReceiver(s *ast.SelectorExpr) string {
switch x := s.X.(type) {
case *ast.Ident:
return x.Name
case *ast.SelectorExpr:
return selReceiver(x) + "." + x.Sel.Name
}
return "?"
}
// selfCallsOf 收集形如 x.method(...) 的调用(x 通常是接收者变量名)。
func selfCallsOf(body *ast.BlockStmt) []string {
var out []string
ast.Inspect(body, func(n ast.Node) bool {
c, ok := n.(*ast.CallExpr)
if !ok {
return true
}
if sel, ok := c.Fun.(*ast.SelectorExpr); ok {
if _, isIdent := sel.X.(*ast.Ident); isIdent {
out = append(out, sel.Sel.Name)
}
}
return true
})
return out
}
func hasMutexIn(body *ast.BlockStmt) bool {
found := false
ast.Inspect(body, func(n ast.Node) bool {
if c, ok := n.(*ast.CallExpr); ok {
s := callString(c)
if strings.Contains(s, "Lock") || strings.Contains(s, "RLock") {
found = true
}
}
return true
})
return found
}
func report(all map[string]*toolAudit) {
names := make([]string, 0, len(all))
for n := range all {
names = append(names, n)
}
sort.Strings(names)
var safe, serial, unknown int
for _, n := range names {
ta := all[n]
switch ta.verdict {
case verdictSafe:
safe++
case verdictSerial:
serial++
default:
unknown++
}
fmt.Printf("%-8s %-28s %s", ta.verdict, n, ta.file)
if ta.implFile != "" {
fmt.Printf(" → %s:%d", ta.implFile, ta.implLine)
}
for _, f := range ta.findings {
if f.line == 0 {
fmt.Printf("\n · %s", f.what)
}
}
fmt.Println()
}
fmt.Printf("\n共 %d:SAFE %d / SERIAL %d / UNKNOWN %d\n", len(all), safe, serial, unknown)
fmt.Println("⚠ UNKNOWN 一律不声明并发安全 —— 追不到实现就不能声称安全。")
}