// main.go - CLI entry for the dead-field scanner. // // Usage from core/: // // go run ./tools/dead-field-scan -root=. // // Or via Makefile target `make dead-field-scan`. Exit code 0 for POC mode // (always reports findings, never fails the build); once CI gating is // agreed on this will switch to exit 1 on any finding plus a whitelist // file to silence intentional cases (JSON-only fields, SDK input structs // where consumer modules read the field, etc.). // // main.go - dead-field scanner 的 CLI 入口. // // core/ 下使用: // // go run ./tools/dead-field-scan -root=. // // 或通过 Makefile 目标 `make dead-field-scan`. POC 模式 exit 0 (只报告, // 不 fail build); CI gate 商议定下后切成有发现即 exit 1, 并配 whitelist // 文件抑制有意保留的案例 (JSON-only 字段, 消费方模块读的 SDK input struct // 等). package main import ( "encoding/json" "flag" "fmt" "os" "path/filepath" ) func main() { root := flag.String("root", ".", "directory containing the Go module to scan (runs packages.Load from here)") pattern := flag.String("pattern", "./...", "Go package pattern relative to -root (use ./... to include internal/ and cmd/; ./pkg/... limits to SDK-facing code)") asJSON := flag.Bool("json", false, "emit findings as JSON array (one object per field) for downstream triage tools") flag.Parse() abs, err := filepath.Abs(*root) if err != nil { fmt.Fprintln(os.Stderr, "abs path:", err) os.Exit(2) } s := &Scanner{ Dir: abs, Patterns: []string{*pattern}, } dead, err := s.Scan() if err != nil { fmt.Fprintln(os.Stderr, "scan failed:", err) os.Exit(2) } if *asJSON { // JSON mode feeds downstream triage tools (LLM classifier, CI // gate diffing vs whitelist). Keep Pos separate from the rest // so the triage tool can cite file:line without re-parsing. // // JSON 模式喂给下游 triage 工具 (LLM 分类器, CI gate 跟 whitelist // 对 diff). Pos 单独给出, triage 工具不必重新解析就能 cite // file:line. type jsonRow struct { Pkg string `json:"pkg"` Struct string `json:"struct"` Field string `json:"field"` Tag string `json:"tag,omitempty"` TypeStr string `json:"type"` File string `json:"file"` Line int `json:"line"` } rows := make([]jsonRow, 0, len(dead)) for _, f := range dead { rows = append(rows, jsonRow{ Pkg: f.Pkg, Struct: f.Struct, Field: f.Field, Tag: f.Tag, TypeStr: f.TypeStr, File: f.Pos.Filename, Line: f.Pos.Line, }) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(rows); err != nil { fmt.Fprintln(os.Stderr, "json encode:", err) os.Exit(2) } return } if len(dead) == 0 { fmt.Println("no dead exported fields found") return } fmt.Printf("dead exported fields: %d\n\n", len(dead)) for _, f := range dead { // Use file:line format for editor jumpability; Pos.String // already yields "path:line:col". // // 用 file:line 格式方便编辑器跳转; Pos.String 本就是 // "path:line:col". fmt.Printf("%s.%s.%s\n type %s\n tag %q\n at %s\n", f.Pkg, f.Struct, f.Field, f.TypeStr, f.Tag, f.Pos) } }