|
|
@@ -0,0 +1,540 @@
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "flag"
|
|
|
+ "fmt"
|
|
|
+ "go/parser"
|
|
|
+ "go/token"
|
|
|
+ "os"
|
|
|
+ "path/filepath"
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+)
|
|
|
+
|
|
|
+// DependencyGraph 表示依赖关系图
|
|
|
+type DependencyGraph struct {
|
|
|
+ Packages map[string]*PackageInfo
|
|
|
+ Mutex sync.RWMutex
|
|
|
+}
|
|
|
+
|
|
|
+// PackageInfo 表示包的信息
|
|
|
+type PackageInfo struct {
|
|
|
+ Name string
|
|
|
+ Path string
|
|
|
+ Imports []string
|
|
|
+ ImportedBy []string
|
|
|
+ IsExternal bool
|
|
|
+ IsStandard bool
|
|
|
+}
|
|
|
+
|
|
|
+// DependencyEdge 表示依赖边
|
|
|
+type DependencyEdge struct {
|
|
|
+ From string `json:"from"`
|
|
|
+ To string `json:"to"`
|
|
|
+}
|
|
|
+
|
|
|
+// DependencyOutput 输出结构
|
|
|
+type DependencyOutput struct {
|
|
|
+ Packages []PackageOutput `json:"packages"`
|
|
|
+ Edges []DependencyEdge `json:"edges"`
|
|
|
+}
|
|
|
+
|
|
|
+// PackageOutput 包输出结构
|
|
|
+type PackageOutput struct {
|
|
|
+ Name string `json:"name"`
|
|
|
+ Path string `json:"path"`
|
|
|
+ Imports []string `json:"imports"`
|
|
|
+ ImportedBy []string `json:"imported_by,omitempty"`
|
|
|
+ IsExternal bool `json:"is_external"`
|
|
|
+ IsStandard bool `json:"is_standard"`
|
|
|
+}
|
|
|
+
|
|
|
+func main() {
|
|
|
+ // 解析命令行参数
|
|
|
+ rootDir := flag.String("dir", ".", "要扫描的Go工程目录")
|
|
|
+ outputFormat := flag.String("format", "text", "输出格式: text, json, dot, csv")
|
|
|
+ outputFile := flag.String("output", "", "输出文件路径(默认输出到控制台)")
|
|
|
+ ignoreStdLib := flag.Bool("ignore-std", false, "忽略标准库依赖")
|
|
|
+ includeTests := flag.Bool("include-tests", false, "包含测试文件")
|
|
|
+ flag.Parse()
|
|
|
+
|
|
|
+ // 检查目录是否存在
|
|
|
+ if _, err := os.Stat(*rootDir); os.IsNotExist(err) {
|
|
|
+ fmt.Printf("错误: 目录不存在: %s\n", *rootDir)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建依赖图
|
|
|
+ graph := &DependencyGraph{
|
|
|
+ Packages: make(map[string]*PackageInfo),
|
|
|
+ }
|
|
|
+
|
|
|
+ fmt.Printf("正在扫描目录: %s\n", *rootDir)
|
|
|
+
|
|
|
+ // 扫描Go文件
|
|
|
+ err := scanDirectory(*rootDir, graph, *includeTests)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("扫描错误: %v\n", err)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算导入关系
|
|
|
+ calculateImportedBy(graph)
|
|
|
+
|
|
|
+ // 生成输出
|
|
|
+ output := generateOutput(graph, *ignoreStdLib)
|
|
|
+
|
|
|
+ // 输出结果
|
|
|
+ err = outputResult(output, *outputFormat, *outputFile)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("输出错误: %v\n", err)
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ fmt.Printf("\n扫描完成! 共发现 %d 个包,%d 个依赖关系\n",
|
|
|
+ len(output.Packages), len(output.Edges))
|
|
|
+}
|
|
|
+
|
|
|
+// scanDirectory 递归扫描目录
|
|
|
+func scanDirectory(dir string, graph *DependencyGraph, includeTests bool) error {
|
|
|
+ return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // 跳过vendor目录和隐藏目录
|
|
|
+ if info.IsDir() {
|
|
|
+ if strings.Contains(path, "vendor") || strings.HasPrefix(filepath.Base(path), ".") {
|
|
|
+ return filepath.SkipDir
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只处理.go文件
|
|
|
+ if filepath.Ext(path) != ".go" {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 可选跳过测试文件
|
|
|
+ if !includeTests && strings.HasSuffix(path, "_test.go") {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析Go文件
|
|
|
+ analyzeGoFile(path, graph)
|
|
|
+ return nil
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// analyzeGoFile 分析单个Go文件
|
|
|
+func analyzeGoFile(filePath string, graph *DependencyGraph) {
|
|
|
+ fset := token.NewFileSet()
|
|
|
+ node, err := parser.ParseFile(fset, filePath, nil, parser.ImportsOnly)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("警告: 无法解析文件 %s: %v\n", filePath, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取包名和相对路径
|
|
|
+ relPath, _ := filepath.Rel(".", filepath.Dir(filePath))
|
|
|
+ if relPath == "." {
|
|
|
+ relPath = ""
|
|
|
+ }
|
|
|
+ packagePath := relPath
|
|
|
+ if packagePath == "" {
|
|
|
+ packagePath = "."
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建或获取包信息
|
|
|
+ graph.Mutex.Lock()
|
|
|
+ packageInfo, exists := graph.Packages[packagePath]
|
|
|
+ if !exists {
|
|
|
+ packageInfo = &PackageInfo{
|
|
|
+ Name: node.Name.Name,
|
|
|
+ Path: packagePath,
|
|
|
+ Imports: []string{},
|
|
|
+ ImportedBy: []string{},
|
|
|
+ }
|
|
|
+ graph.Packages[packagePath] = packageInfo
|
|
|
+ }
|
|
|
+ graph.Mutex.Unlock()
|
|
|
+
|
|
|
+ // 收集导入
|
|
|
+ var imports []string
|
|
|
+ for _, imp := range node.Imports {
|
|
|
+ importPath := strings.Trim(imp.Path.Value, `"`)
|
|
|
+ imports = append(imports, importPath)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新导入列表(去重)
|
|
|
+ graph.Mutex.Lock()
|
|
|
+ existingImports := make(map[string]bool)
|
|
|
+ for _, imp := range packageInfo.Imports {
|
|
|
+ existingImports[imp] = true
|
|
|
+ }
|
|
|
+ for _, imp := range imports {
|
|
|
+ if !existingImports[imp] {
|
|
|
+ packageInfo.Imports = append(packageInfo.Imports, imp)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ graph.Mutex.Unlock()
|
|
|
+}
|
|
|
+
|
|
|
+// calculateImportedBy 计算每个包被哪些包导入
|
|
|
+func calculateImportedBy(graph *DependencyGraph) {
|
|
|
+ graph.Mutex.Lock()
|
|
|
+ defer graph.Mutex.Unlock()
|
|
|
+
|
|
|
+ // 清空现有的importedBy
|
|
|
+ for _, pkg := range graph.Packages {
|
|
|
+ pkg.ImportedBy = []string{}
|
|
|
+ pkg.IsExternal = false
|
|
|
+ pkg.IsStandard = true // 先假设是标准库,后续会修正
|
|
|
+ }
|
|
|
+
|
|
|
+ // 遍历所有包,构建导入关系
|
|
|
+ for pkgPath, pkg := range graph.Packages {
|
|
|
+ for _, imp := range pkg.Imports {
|
|
|
+ // 标记外部包和标准库
|
|
|
+ if isStandardPackage(imp) {
|
|
|
+ // 如果是标准库,检查是否在已有包中
|
|
|
+ if _, exists := graph.Packages[imp]; !exists {
|
|
|
+ graph.Packages[imp] = &PackageInfo{
|
|
|
+ Name: filepath.Base(imp),
|
|
|
+ Path: imp,
|
|
|
+ IsStandard: true,
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 外部包
|
|
|
+ if importedPkg, exists := graph.Packages[imp]; exists {
|
|
|
+ importedPkg.IsExternal = true
|
|
|
+ importedPkg.IsStandard = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加importedBy关系
|
|
|
+ if importedPkg, exists := graph.Packages[imp]; exists {
|
|
|
+ importedPkg.ImportedBy = append(importedPkg.ImportedBy, pkgPath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 标记项目内部的包
|
|
|
+ for pkgPath, pkg := range graph.Packages {
|
|
|
+ if _, exists := graph.Packages[pkgPath]; exists && !pkg.IsExternal {
|
|
|
+ // 如果路径中包含.或者没有/,可能是本地包
|
|
|
+ if strings.Contains(pkgPath, ".") || !strings.Contains(pkgPath, "/") {
|
|
|
+ pkg.IsStandard = true
|
|
|
+ } else {
|
|
|
+ pkg.IsStandard = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// isStandardPackage 检查是否是标准库包
|
|
|
+func isStandardPackage(pkgPath string) bool {
|
|
|
+ // 标准库通常不包含域名
|
|
|
+ if strings.Contains(pkgPath, ".") {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // 常见的一级标准库目录
|
|
|
+ stdLibs := map[string]bool{
|
|
|
+ "fmt": true,
|
|
|
+ "io": true,
|
|
|
+ "net": true,
|
|
|
+ "http": true,
|
|
|
+ "os": true,
|
|
|
+ "strings": true,
|
|
|
+ "strconv": true,
|
|
|
+ "encoding": true,
|
|
|
+ "json": true,
|
|
|
+ "xml": true,
|
|
|
+ "time": true,
|
|
|
+ "sync": true,
|
|
|
+ "math": true,
|
|
|
+ "sort": true,
|
|
|
+ "container": true,
|
|
|
+ "crypto": true,
|
|
|
+ "database": true,
|
|
|
+ "debug": true,
|
|
|
+ "embed": true,
|
|
|
+ "errors": true,
|
|
|
+ "expvar": true,
|
|
|
+ "flag": true,
|
|
|
+ "go": true,
|
|
|
+ "hash": true,
|
|
|
+ "html": true,
|
|
|
+ "image": true,
|
|
|
+ "index": true,
|
|
|
+ "log": true,
|
|
|
+ "mime": true,
|
|
|
+ "path": true,
|
|
|
+ "reflect": true,
|
|
|
+ "regexp": true,
|
|
|
+ "runtime": true,
|
|
|
+ "testing": true,
|
|
|
+ "text": true,
|
|
|
+ "unicode": true,
|
|
|
+ "unsafe": true,
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取第一级目录
|
|
|
+ firstPart := pkgPath
|
|
|
+ if idx := strings.Index(pkgPath, "/"); idx != -1 {
|
|
|
+ firstPart = pkgPath[:idx]
|
|
|
+ }
|
|
|
+
|
|
|
+ return stdLibs[firstPart]
|
|
|
+}
|
|
|
+
|
|
|
+// generateOutput 生成输出数据
|
|
|
+func generateOutput(graph *DependencyGraph, ignoreStdLib bool) *DependencyOutput {
|
|
|
+ graph.Mutex.RLock()
|
|
|
+ defer graph.Mutex.RUnlock()
|
|
|
+
|
|
|
+ output := &DependencyOutput{
|
|
|
+ Packages: []PackageOutput{},
|
|
|
+ Edges: []DependencyEdge{},
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集包信息
|
|
|
+ var packagePaths []string
|
|
|
+ for pkgPath := range graph.Packages {
|
|
|
+ packagePaths = append(packagePaths, pkgPath)
|
|
|
+ }
|
|
|
+ sort.Strings(packagePaths)
|
|
|
+
|
|
|
+ for _, pkgPath := range packagePaths {
|
|
|
+ pkg := graph.Packages[pkgPath]
|
|
|
+
|
|
|
+ // 如果忽略标准库且当前包是标准库,则跳过
|
|
|
+ if ignoreStdLib && pkg.IsStandard {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加包信息
|
|
|
+ pkgOutput := PackageOutput{
|
|
|
+ Name: pkg.Name,
|
|
|
+ Path: pkg.Path,
|
|
|
+ Imports: make([]string, len(pkg.Imports)),
|
|
|
+ ImportedBy: make([]string, len(pkg.ImportedBy)),
|
|
|
+ IsExternal: pkg.IsExternal,
|
|
|
+ IsStandard: pkg.IsStandard,
|
|
|
+ }
|
|
|
+ copy(pkgOutput.Imports, pkg.Imports)
|
|
|
+ copy(pkgOutput.ImportedBy, pkg.ImportedBy)
|
|
|
+ output.Packages = append(output.Packages, pkgOutput)
|
|
|
+
|
|
|
+ // 添加依赖边(排除指向被忽略的标准库的边)
|
|
|
+ for _, imp := range pkg.Imports {
|
|
|
+ if importedPkg, exists := graph.Packages[imp]; exists {
|
|
|
+ if ignoreStdLib && importedPkg.IsStandard {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ output.Edges = append(output.Edges, DependencyEdge{
|
|
|
+ From: pkgPath,
|
|
|
+ To: imp,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return output
|
|
|
+}
|
|
|
+
|
|
|
+// outputResult 输出结果
|
|
|
+func outputResult(output *DependencyOutput, format string, outputFile string) error {
|
|
|
+ var result string
|
|
|
+ var err error
|
|
|
+
|
|
|
+ switch format {
|
|
|
+ case "json":
|
|
|
+ result, err = outputJSON(output)
|
|
|
+ case "dot":
|
|
|
+ result, err = outputDOT(output)
|
|
|
+ case "csv":
|
|
|
+ result, err = outputCSV(output)
|
|
|
+ default: // text
|
|
|
+ result, err = outputText(output)
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // 输出到文件或控制台
|
|
|
+ if outputFile != "" {
|
|
|
+ err = os.WriteFile(outputFile, []byte(result), 0644)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ fmt.Printf("结果已保存到: %s\n", outputFile)
|
|
|
+ } else {
|
|
|
+ fmt.Println(result)
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// outputText 文本格式输出
|
|
|
+func outputText(output *DependencyOutput) (string, error) {
|
|
|
+ var builder strings.Builder
|
|
|
+
|
|
|
+ builder.WriteString("=== 包依赖分析报告 ===\n\n")
|
|
|
+
|
|
|
+ // 按类型分组
|
|
|
+ var stdPackages, localPackages, extPackages []PackageOutput
|
|
|
+ for _, pkg := range output.Packages {
|
|
|
+ if pkg.IsStandard {
|
|
|
+ stdPackages = append(stdPackages, pkg)
|
|
|
+ } else if pkg.IsExternal {
|
|
|
+ extPackages = append(extPackages, pkg)
|
|
|
+ } else {
|
|
|
+ localPackages = append(localPackages, pkg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 输出本地包
|
|
|
+ if len(localPackages) > 0 {
|
|
|
+ builder.WriteString("本地包:\n")
|
|
|
+ for _, pkg := range localPackages {
|
|
|
+ builder.WriteString(fmt.Sprintf(" %s (%s)\n", pkg.Path, pkg.Name))
|
|
|
+ if len(pkg.Imports) > 0 {
|
|
|
+ builder.WriteString(" 导入:\n")
|
|
|
+ for _, imp := range pkg.Imports {
|
|
|
+ builder.WriteString(fmt.Sprintf(" - %s\n", imp))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if len(pkg.ImportedBy) > 0 {
|
|
|
+ builder.WriteString(" 被以下包导入:\n")
|
|
|
+ for _, by := range pkg.ImportedBy {
|
|
|
+ builder.WriteString(fmt.Sprintf(" - %s\n", by))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ builder.WriteString("\n")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 输出外部包
|
|
|
+ if len(extPackages) > 0 {
|
|
|
+ builder.WriteString("外部依赖:\n")
|
|
|
+ for _, pkg := range extPackages {
|
|
|
+ builder.WriteString(fmt.Sprintf(" %s\n", pkg.Path))
|
|
|
+ if len(pkg.ImportedBy) > 0 {
|
|
|
+ builder.WriteString(" 被以下包导入:\n")
|
|
|
+ for _, by := range pkg.ImportedBy {
|
|
|
+ builder.WriteString(fmt.Sprintf(" - %s\n", by))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ builder.WriteString("\n")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 输出标准库(简略)
|
|
|
+ if len(stdPackages) > 0 {
|
|
|
+ builder.WriteString("标准库依赖:\n")
|
|
|
+ libs := make(map[string]bool)
|
|
|
+ for _, pkg := range stdPackages {
|
|
|
+ libs[pkg.Path] = true
|
|
|
+ }
|
|
|
+ var libList []string
|
|
|
+ for lib := range libs {
|
|
|
+ libList = append(libList, lib)
|
|
|
+ }
|
|
|
+ sort.Strings(libList)
|
|
|
+ for _, lib := range libList {
|
|
|
+ builder.WriteString(fmt.Sprintf(" %s\n", lib))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 依赖统计
|
|
|
+ builder.WriteString("\n=== 依赖统计 ===\n")
|
|
|
+ builder.WriteString(fmt.Sprintf("总包数: %d\n", len(output.Packages)))
|
|
|
+ builder.WriteString(fmt.Sprintf("本地包: %d\n", len(localPackages)))
|
|
|
+ builder.WriteString(fmt.Sprintf("外部包: %d\n", len(extPackages)))
|
|
|
+ builder.WriteString(fmt.Sprintf("标准库: %d\n", len(stdPackages)))
|
|
|
+ builder.WriteString(fmt.Sprintf("依赖关系数: %d\n", len(output.Edges)))
|
|
|
+
|
|
|
+ return builder.String(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// outputJSON JSON格式输出
|
|
|
+func outputJSON(output *DependencyOutput) (string, error) {
|
|
|
+ data, err := json.MarshalIndent(output, "", " ")
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ return string(data), nil
|
|
|
+}
|
|
|
+
|
|
|
+// outputDOT DOT格式输出(用于Graphviz)
|
|
|
+func outputDOT(output *DependencyOutput) (string, error) {
|
|
|
+ var builder strings.Builder
|
|
|
+
|
|
|
+ builder.WriteString("digraph GoDependencies {\n")
|
|
|
+ builder.WriteString(" rankdir=LR;\n")
|
|
|
+ builder.WriteString(" node [shape=box, style=filled];\n\n")
|
|
|
+
|
|
|
+ // 定义节点
|
|
|
+ for _, pkg := range output.Packages {
|
|
|
+ color := "lightblue"
|
|
|
+ if pkg.IsExternal {
|
|
|
+ color = "lightcoral"
|
|
|
+ } else if pkg.IsStandard {
|
|
|
+ color = "lightgrey"
|
|
|
+ }
|
|
|
+
|
|
|
+ builder.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\", fillcolor=\"%s\"];\n",
|
|
|
+ pkg.Path, pkg.Path, color))
|
|
|
+ }
|
|
|
+
|
|
|
+ builder.WriteString("\n")
|
|
|
+
|
|
|
+ // 定义边
|
|
|
+ for _, edge := range output.Edges {
|
|
|
+ builder.WriteString(fmt.Sprintf(" \"%s\" -> \"%s\";\n", edge.From, edge.To))
|
|
|
+ }
|
|
|
+
|
|
|
+ builder.WriteString("}\n")
|
|
|
+ return builder.String(), nil
|
|
|
+}
|
|
|
+
|
|
|
+// outputCSV CSV格式输出
|
|
|
+func outputCSV(output *DependencyOutput) (string, error) {
|
|
|
+ var builder strings.Builder
|
|
|
+
|
|
|
+ // 写表头
|
|
|
+ builder.WriteString("From,To,Type\n")
|
|
|
+
|
|
|
+ // 写数据
|
|
|
+ for _, edge := range output.Edges {
|
|
|
+ toPkg := findPackage(output.Packages, edge.To)
|
|
|
+
|
|
|
+ depType := "local"
|
|
|
+ if toPkg != nil {
|
|
|
+ if toPkg.IsExternal {
|
|
|
+ depType = "external"
|
|
|
+ } else if toPkg.IsStandard {
|
|
|
+ depType = "standard"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ builder.WriteString(fmt.Sprintf("%s,%s,%s\n", edge.From, edge.To, depType))
|
|
|
+ }
|
|
|
+
|
|
|
+ return builder.String(), nil
|
|
|
+}
|
|
|
+
|
|
|
+func findPackage(packages []PackageOutput, path string) *PackageOutput {
|
|
|
+ for _, pkg := range packages {
|
|
|
+ if pkg.Path == path {
|
|
|
+ return &pkg
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|