mirror of
https://gitee.com/johng/gf
synced 2026-07-06 13:42:46 +08:00
feat: 新增模块级依赖分析功能
This commit is contained in:
@ -39,13 +39,13 @@ gf dep -r
|
||||
gf dep -i=false
|
||||
gf dep -e
|
||||
gf dep -e -i=false
|
||||
gf dep -m
|
||||
gf dep -m -e
|
||||
gf dep -M
|
||||
gf dep -M -D
|
||||
gf dep -s
|
||||
gf dep -s -p 8080
|
||||
gf dep ./internal/... -f tree -d 2
|
||||
gf dep --external --group -f mermaid
|
||||
gf dep --main --external -f json
|
||||
gf dep --module --direct -f json
|
||||
`
|
||||
)
|
||||
|
||||
@ -62,12 +62,13 @@ type Input struct {
|
||||
Package string `name:"PACKAGE" arg:"true" brief:"package path to analyze, default is ./..." d:"./..."`
|
||||
Format string `name:"format" short:"f" brief:"output format: tree/list/mermaid/dot/json" d:"tree"`
|
||||
Depth int `name:"depth" short:"d" brief:"dependency depth limit, 0 means unlimited" d:"3"`
|
||||
Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false"`
|
||||
Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true"`
|
||||
External bool `name:"external" short:"e" brief:"show external packages" d:"false"`
|
||||
MainOnly bool `name:"main" short:"m" brief:"analyze only main module packages (exclude submodules)" d:"false"`
|
||||
NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true"`
|
||||
Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false"`
|
||||
Group bool `name:"group" short:"g" brief:"group by top-level directory" d:"false" orphan:"true"`
|
||||
Internal bool `name:"internal" short:"i" brief:"show only internal packages" d:"true" orphan:"true"`
|
||||
External bool `name:"external" short:"e" brief:"show external packages" d:"false" orphan:"true"`
|
||||
Module bool `name:"module" short:"M" brief:"show module-level dependencies (from go.mod)" d:"false" orphan:"true"`
|
||||
Direct bool `name:"direct" short:"D" brief:"show only direct dependencies (requires --module)" d:"false" orphan:"true"`
|
||||
NoStd bool `name:"nostd" short:"n" brief:"exclude standard library" d:"true" orphan:"true"`
|
||||
Reverse bool `name:"reverse" short:"r" brief:"show reverse dependencies" d:"false" orphan:"true"`
|
||||
Serve bool `name:"serve" short:"s" brief:"start HTTP server to view dependencies" d:"false" orphan:"true"`
|
||||
Port int `name:"port" short:"p" brief:"HTTP server port" d:"8888"`
|
||||
}
|
||||
@ -82,7 +83,28 @@ func (c cDep) Index(ctx context.Context, in Input) (out *Output, err error) {
|
||||
// Detect module prefix from go.mod
|
||||
analyzer.modulePrefix = analyzer.detectModulePrefix()
|
||||
|
||||
// Get package information
|
||||
// Module-level analysis mode (uses go mod graph)
|
||||
if in.Module {
|
||||
if in.Serve {
|
||||
// Load module graph for server mode
|
||||
if err := analyzer.loadModuleGraph(ctx); err != nil {
|
||||
mlog.Print("Warning: Failed to load module graph: " + err.Error())
|
||||
}
|
||||
return nil, analyzer.startServer(in)
|
||||
}
|
||||
|
||||
// Load module graph
|
||||
if err := analyzer.loadModuleGraph(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate module-level output
|
||||
output := analyzer.generateModuleOutput(in)
|
||||
mlog.Print(output)
|
||||
return
|
||||
}
|
||||
|
||||
// Package-level analysis mode (original behavior)
|
||||
loadErr := analyzer.loadPackages(ctx, in.Package)
|
||||
|
||||
// Start HTTP server if requested
|
||||
|
||||
@ -49,31 +49,29 @@ const (
|
||||
// It consolidates package information from go list output with additional
|
||||
// metadata for filtering and traversal.
|
||||
type PackageInfo struct {
|
||||
ImportPath string // Full import path (e.g., github.com/gogf/gf/v2/os/gfile)
|
||||
ModulePath string // Module path (e.g., github.com/gogf/gf/v2)
|
||||
Kind PackageKind // Package classification (Internal/External/StdLib)
|
||||
Tier int // Package tier: 0=module root, 1=top-level, 2+=nested
|
||||
Imports []string // Direct imports of this package
|
||||
IsStdLib bool // Standard library marker (from go list)
|
||||
IsModuleRoot bool // Is this the root package of its module
|
||||
IsMainModule bool // Is this package from the main module (not a submodule)
|
||||
ImportPath string // Full import path (e.g., github.com/gogf/gf/v2/os/gfile)
|
||||
ModulePath string // Module path (e.g., github.com/gogf/gf/v2)
|
||||
Kind PackageKind // Package classification (Internal/External/StdLib)
|
||||
Tier int // Package tier: 0=module root, 1=top-level, 2+=nested
|
||||
Imports []string // Direct imports of this package
|
||||
IsStdLib bool // Standard library marker (from go list)
|
||||
IsModuleRoot bool // Is this the root package of its module
|
||||
}
|
||||
|
||||
// FilterOptions represents filtering criteria for dependency analysis.
|
||||
// It provides a clear, normalized representation of user filtering preferences.
|
||||
// Usage:
|
||||
// opts := &FilterOptions{
|
||||
// IncludeInternal: true,
|
||||
// IncludeExternal: false,
|
||||
// IncludeStdLib: false,
|
||||
// MainModuleOnly: false,
|
||||
// Depth: 3,
|
||||
// }
|
||||
//
|
||||
// opts := &FilterOptions{
|
||||
// IncludeInternal: true,
|
||||
// IncludeExternal: false,
|
||||
// IncludeStdLib: false,
|
||||
// Depth: 3,
|
||||
// }
|
||||
type FilterOptions struct {
|
||||
IncludeInternal bool // Include internal packages from main module
|
||||
IncludeExternal bool // Include external dependencies
|
||||
IncludeStdLib bool // Include standard library packages
|
||||
MainModuleOnly bool // Only analyze packages from main module (exclude submodules)
|
||||
Depth int // Maximum traversal depth (0 = unlimited)
|
||||
}
|
||||
|
||||
@ -106,15 +104,30 @@ type analyzer struct {
|
||||
visited map[string]bool
|
||||
edges map[string]bool
|
||||
store *PackageStore // New unified package store
|
||||
// Module-level dependency data (from go mod graph)
|
||||
modules map[string]*ModuleInfo // All modules indexed by path
|
||||
moduleGraph map[string][]string // Module dependency graph: module -> dependencies
|
||||
directModules map[string]bool // Direct dependencies (from go.mod require)
|
||||
}
|
||||
|
||||
// ModuleInfo represents a Go module dependency.
|
||||
type ModuleInfo struct {
|
||||
Path string // Module path (e.g., github.com/gin-gonic/gin)
|
||||
Version string // Module version (e.g., v1.9.0)
|
||||
IsDirect bool // Is this a direct dependency
|
||||
Deps []string // Dependencies of this module
|
||||
}
|
||||
|
||||
// newAnalyzer creates a new dependency analyzer.
|
||||
func newAnalyzer() *analyzer {
|
||||
return &analyzer{
|
||||
packages: make(map[string]*goPackage),
|
||||
visited: make(map[string]bool),
|
||||
edges: make(map[string]bool),
|
||||
store: &PackageStore{},
|
||||
packages: make(map[string]*goPackage),
|
||||
visited: make(map[string]bool),
|
||||
edges: make(map[string]bool),
|
||||
store: &PackageStore{},
|
||||
modules: make(map[string]*ModuleInfo),
|
||||
moduleGraph: make(map[string][]string),
|
||||
directModules: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +233,423 @@ func (a *analyzer) loadPackages(ctx context.Context, pkgPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadModuleGraph loads module-level dependencies using go mod graph.
|
||||
// This provides module-level dependency information (not package-level).
|
||||
func (a *analyzer) loadModuleGraph(ctx context.Context) error {
|
||||
// First, get direct dependencies from go list -m
|
||||
directCmd := "go list -m -json all"
|
||||
directResult, err := gproc.ShellExec(ctx, directCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute go list -m: %v", err)
|
||||
}
|
||||
|
||||
// Parse direct dependencies
|
||||
directDecoder := json.NewDecoder(strings.NewReader(directResult))
|
||||
for directDecoder.More() {
|
||||
var mod struct {
|
||||
Path string `json:"Path"`
|
||||
Version string `json:"Version"`
|
||||
Main bool `json:"Main"`
|
||||
Indirect bool `json:"Indirect"`
|
||||
}
|
||||
if err := directDecoder.Decode(&mod); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip main module
|
||||
if mod.Main {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track direct vs indirect
|
||||
if !mod.Indirect {
|
||||
a.directModules[mod.Path] = true
|
||||
}
|
||||
|
||||
// Create module info
|
||||
a.modules[mod.Path] = &ModuleInfo{
|
||||
Path: mod.Path,
|
||||
Version: mod.Version,
|
||||
IsDirect: !mod.Indirect,
|
||||
Deps: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Then, get module dependency graph
|
||||
graphCmd := "go mod graph"
|
||||
graphResult, err := gproc.ShellExec(ctx, graphCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute go mod graph: %v", err)
|
||||
}
|
||||
|
||||
// Parse go mod graph output
|
||||
// Format: module1@version module2@version
|
||||
lines := gstr.Split(gstr.Trim(graphResult), "\n")
|
||||
for _, line := range lines {
|
||||
line = gstr.Trim(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := gstr.Split(line, " ")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
from := a.parseModulePath(parts[0])
|
||||
to := a.parseModulePath(parts[1])
|
||||
|
||||
// Add to graph
|
||||
a.moduleGraph[from] = append(a.moduleGraph[from], to)
|
||||
|
||||
// Ensure both modules exist in our map
|
||||
if _, ok := a.modules[from]; !ok {
|
||||
a.modules[from] = &ModuleInfo{
|
||||
Path: from,
|
||||
Version: a.parseModuleVersion(parts[0]),
|
||||
IsDirect: a.directModules[from],
|
||||
}
|
||||
}
|
||||
if _, ok := a.modules[to]; !ok {
|
||||
a.modules[to] = &ModuleInfo{
|
||||
Path: to,
|
||||
Version: a.parseModuleVersion(parts[1]),
|
||||
IsDirect: a.directModules[to],
|
||||
}
|
||||
}
|
||||
|
||||
// Update deps
|
||||
a.modules[from].Deps = append(a.modules[from].Deps, to)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseModulePath extracts module path from "module@version" format.
|
||||
func (a *analyzer) parseModulePath(s string) string {
|
||||
if idx := gstr.Pos(s, "@"); idx > 0 {
|
||||
return s[:idx]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// parseModuleVersion extracts version from "module@version" format.
|
||||
func (a *analyzer) parseModuleVersion(s string) string {
|
||||
if idx := gstr.Pos(s, "@"); idx > 0 {
|
||||
return s[idx+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// generateModuleOutput generates output for module-level dependencies.
|
||||
func (a *analyzer) generateModuleOutput(in Input) string {
|
||||
switch in.Format {
|
||||
case "tree":
|
||||
return a.generateModuleTree(in)
|
||||
case "list":
|
||||
return a.generateModuleList(in)
|
||||
case "mermaid":
|
||||
return a.generateModuleMermaid(in)
|
||||
case "dot":
|
||||
return a.generateModuleDot(in)
|
||||
case "json":
|
||||
return a.generateModuleJSON(in)
|
||||
default:
|
||||
return a.generateModuleTree(in)
|
||||
}
|
||||
}
|
||||
|
||||
// generateModuleTree generates tree output for module dependencies.
|
||||
func (a *analyzer) generateModuleTree(in Input) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Get modules to display
|
||||
modules := a.getFilteredModules(in)
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%s (module dependencies)\n", a.modulePrefix))
|
||||
|
||||
// Show direct dependencies first
|
||||
directDeps := make([]string, 0)
|
||||
for _, mod := range modules {
|
||||
if mod.IsDirect {
|
||||
directDeps = append(directDeps, mod.Path)
|
||||
}
|
||||
}
|
||||
sort.Strings(directDeps)
|
||||
|
||||
visited := make(map[string]bool)
|
||||
for i, dep := range directDeps {
|
||||
isLast := i == len(directDeps)-1
|
||||
a.printModuleTreeNode(&sb, dep, "", isLast, in, visited, 0)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// printModuleTreeNode prints a module tree node.
|
||||
func (a *analyzer) printModuleTreeNode(sb *strings.Builder, modPath string, prefix string, isLast bool, in Input, visited map[string]bool, depth int) {
|
||||
if in.Depth > 0 && depth >= in.Depth {
|
||||
return
|
||||
}
|
||||
|
||||
connector := "├── "
|
||||
if isLast {
|
||||
connector = "└── "
|
||||
}
|
||||
|
||||
mod := a.modules[modPath]
|
||||
version := ""
|
||||
if mod != nil && mod.Version != "" {
|
||||
version = "@" + mod.Version
|
||||
}
|
||||
|
||||
sb.WriteString(prefix + connector + modPath + version + "\n")
|
||||
|
||||
// Skip if Direct mode and this is not a direct dependency
|
||||
if in.Direct {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for cycles
|
||||
if visited[modPath] {
|
||||
return
|
||||
}
|
||||
visited[modPath] = true
|
||||
|
||||
// Get dependencies
|
||||
deps := a.moduleGraph[modPath]
|
||||
sort.Strings(deps)
|
||||
|
||||
newPrefix := prefix
|
||||
if isLast {
|
||||
newPrefix += " "
|
||||
} else {
|
||||
newPrefix += "│ "
|
||||
}
|
||||
|
||||
for i, dep := range deps {
|
||||
depIsLast := i == len(deps)-1
|
||||
a.printModuleTreeNode(sb, dep, newPrefix, depIsLast, in, visited, depth+1)
|
||||
}
|
||||
|
||||
delete(visited, modPath)
|
||||
}
|
||||
|
||||
// generateModuleList generates list output for module dependencies.
|
||||
func (a *analyzer) generateModuleList(in Input) string {
|
||||
var sb strings.Builder
|
||||
|
||||
modules := a.getFilteredModules(in)
|
||||
|
||||
// Count stats
|
||||
directCount := 0
|
||||
indirectCount := 0
|
||||
for _, mod := range modules {
|
||||
if mod.IsDirect {
|
||||
directCount++
|
||||
} else {
|
||||
indirectCount++
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Module Dependencies for %s\n", a.modulePrefix))
|
||||
sb.WriteString(fmt.Sprintf("# Direct: %d, Indirect: %d, Total: %d\n\n", directCount, indirectCount, len(modules)))
|
||||
|
||||
// Sort by path
|
||||
paths := make([]string, 0, len(modules))
|
||||
for path := range modules {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
|
||||
for _, path := range paths {
|
||||
mod := modules[path]
|
||||
marker := ""
|
||||
if mod.IsDirect {
|
||||
marker = " (direct)"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s@%s%s\n", mod.Path, mod.Version, marker))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// generateModuleMermaid generates Mermaid diagram for module dependencies.
|
||||
func (a *analyzer) generateModuleMermaid(in Input) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("```mermaid\n")
|
||||
sb.WriteString("graph TD\n")
|
||||
|
||||
edges := a.collectModuleEdges(in)
|
||||
sortedEdges := make([]string, 0, len(edges))
|
||||
for edge := range edges {
|
||||
sortedEdges = append(sortedEdges, edge)
|
||||
}
|
||||
sort.Strings(sortedEdges)
|
||||
|
||||
for _, edge := range sortedEdges {
|
||||
sb.WriteString(" " + edge + "\n")
|
||||
}
|
||||
sb.WriteString("```\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// generateModuleDot generates Graphviz DOT for module dependencies.
|
||||
func (a *analyzer) generateModuleDot(in Input) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("digraph modules {\n")
|
||||
sb.WriteString(" rankdir=TB;\n")
|
||||
sb.WriteString(" node [shape=box];\n")
|
||||
|
||||
edges := a.collectModuleEdges(in)
|
||||
sortedEdges := make([]string, 0, len(edges))
|
||||
for edge := range edges {
|
||||
sortedEdges = append(sortedEdges, edge)
|
||||
}
|
||||
sort.Strings(sortedEdges)
|
||||
|
||||
for _, edge := range sortedEdges {
|
||||
parts := gstr.Split(edge, " --> ")
|
||||
if len(parts) == 2 {
|
||||
fmt.Fprintf(&sb, " \"%s\" -> \"%s\";\n", parts[0], parts[1])
|
||||
}
|
||||
}
|
||||
sb.WriteString("}\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// generateModuleJSON generates JSON output for module dependencies.
|
||||
func (a *analyzer) generateModuleJSON(in Input) string {
|
||||
modules := a.getFilteredModules(in)
|
||||
|
||||
result := make(map[string]any)
|
||||
|
||||
// Build module list
|
||||
moduleList := make([]map[string]any, 0)
|
||||
for _, mod := range modules {
|
||||
m := map[string]any{
|
||||
"path": mod.Path,
|
||||
"version": mod.Version,
|
||||
"direct": mod.IsDirect,
|
||||
"depCount": len(mod.Deps),
|
||||
}
|
||||
if !in.Direct {
|
||||
m["dependencies"] = mod.Deps
|
||||
}
|
||||
moduleList = append(moduleList, m)
|
||||
}
|
||||
|
||||
// Sort by path
|
||||
sort.Slice(moduleList, func(i, j int) bool {
|
||||
return moduleList[i]["path"].(string) < moduleList[j]["path"].(string)
|
||||
})
|
||||
|
||||
result["modules"] = moduleList
|
||||
|
||||
// Add statistics
|
||||
directCount := 0
|
||||
indirectCount := 0
|
||||
for _, mod := range modules {
|
||||
if mod.IsDirect {
|
||||
directCount++
|
||||
} else {
|
||||
indirectCount++
|
||||
}
|
||||
}
|
||||
result["statistics"] = map[string]any{
|
||||
"total": len(modules),
|
||||
"direct": directCount,
|
||||
"indirect": indirectCount,
|
||||
}
|
||||
|
||||
result["metadata"] = map[string]any{
|
||||
"module": a.modulePrefix,
|
||||
"format": in.Format,
|
||||
"depth": in.Depth,
|
||||
"direct": in.Direct,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(result, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// getFilteredModules returns modules based on filter options.
|
||||
func (a *analyzer) getFilteredModules(in Input) map[string]*ModuleInfo {
|
||||
result := make(map[string]*ModuleInfo)
|
||||
|
||||
for path, mod := range a.modules {
|
||||
// Skip main module
|
||||
if path == a.modulePrefix {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by direct/indirect
|
||||
if in.Direct && !mod.IsDirect {
|
||||
continue
|
||||
}
|
||||
|
||||
result[path] = mod
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// collectModuleEdges collects edges for module dependency graph.
|
||||
func (a *analyzer) collectModuleEdges(in Input) map[string]bool {
|
||||
edges := make(map[string]bool)
|
||||
|
||||
// Start from main module
|
||||
if in.Direct {
|
||||
// Only show direct dependencies from main module
|
||||
for path, mod := range a.modules {
|
||||
if mod.IsDirect {
|
||||
from := a.sanitizeName(a.shortModuleName(a.modulePrefix))
|
||||
to := a.sanitizeName(a.shortModuleName(path))
|
||||
edge := fmt.Sprintf("%s --> %s", from, to)
|
||||
edges[edge] = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show full dependency graph
|
||||
visited := make(map[string]bool)
|
||||
a.collectModuleEdgesRecursive(a.modulePrefix, in, edges, visited, 0)
|
||||
}
|
||||
|
||||
return edges
|
||||
}
|
||||
|
||||
// collectModuleEdgesRecursive recursively collects module edges.
|
||||
func (a *analyzer) collectModuleEdgesRecursive(modPath string, in Input, edges map[string]bool, visited map[string]bool, depth int) {
|
||||
if in.Depth > 0 && depth >= in.Depth {
|
||||
return
|
||||
}
|
||||
|
||||
if visited[modPath] {
|
||||
return
|
||||
}
|
||||
visited[modPath] = true
|
||||
|
||||
deps := a.moduleGraph[modPath]
|
||||
fromName := a.sanitizeName(a.shortModuleName(modPath))
|
||||
|
||||
for _, dep := range deps {
|
||||
toName := a.sanitizeName(a.shortModuleName(dep))
|
||||
if fromName != toName {
|
||||
edge := fmt.Sprintf("%s --> %s", fromName, toName)
|
||||
edges[edge] = true
|
||||
}
|
||||
a.collectModuleEdgesRecursive(dep, in, edges, visited, depth+1)
|
||||
}
|
||||
}
|
||||
|
||||
// shortModuleName returns the module name (keeping full domain for clarity).
|
||||
func (a *analyzer) shortModuleName(modPath string) string {
|
||||
// Return full module path to preserve domain information
|
||||
return modPath
|
||||
}
|
||||
|
||||
// convertInputToFilterOptions converts legacy Input to new FilterOptions.
|
||||
func (a *analyzer) convertInputToFilterOptions(in Input) *FilterOptions {
|
||||
@ -228,7 +657,6 @@ func (a *analyzer) convertInputToFilterOptions(in Input) *FilterOptions {
|
||||
IncludeInternal: in.Internal,
|
||||
IncludeExternal: in.External,
|
||||
IncludeStdLib: !in.NoStd,
|
||||
MainModuleOnly: in.MainOnly,
|
||||
Depth: in.Depth,
|
||||
}
|
||||
|
||||
@ -321,11 +749,6 @@ func (opts *FilterOptions) Normalize(modulePrefix string) error {
|
||||
|
||||
// ShouldInclude determines if a package should be included based on filter options.
|
||||
func (opts *FilterOptions) ShouldInclude(pkg *PackageInfo) bool {
|
||||
// Filter by main module only (exclude submodules)
|
||||
if opts.MainModuleOnly && !pkg.IsMainModule {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter by kind
|
||||
switch pkg.Kind {
|
||||
case KindStdLib:
|
||||
@ -383,36 +806,6 @@ func (tc *TraversalContext) GetDependencies(pkg string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// isMainModulePackage checks if a package belongs to the main module (not a submodule).
|
||||
func (a *analyzer) isMainModulePackage(pkg string) bool {
|
||||
if a.modulePrefix == "" {
|
||||
return true // If no module prefix, consider all as main module
|
||||
}
|
||||
|
||||
if !gstr.HasPrefix(pkg, a.modulePrefix) {
|
||||
return false // Not even in our module
|
||||
}
|
||||
|
||||
// Remove the module prefix to get the relative path
|
||||
relativePath := gstr.TrimLeft(pkg[len(a.modulePrefix):], "/")
|
||||
if relativePath == "" {
|
||||
return true // This is the root module itself
|
||||
}
|
||||
|
||||
// Check if this path contains a go.mod file (indicating a submodule)
|
||||
// We check from the most specific path up to the root
|
||||
parts := gstr.Split(relativePath, "/")
|
||||
for i := len(parts); i > 0; i-- {
|
||||
subPath := gstr.Join(parts[:i], "/")
|
||||
if subPath != "" && gfile.Exists(subPath+"/go.mod") {
|
||||
// Found a go.mod file in a subdirectory, this indicates a submodule
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true // This is part of the main module
|
||||
}
|
||||
|
||||
// shortName returns a shortened package name.
|
||||
func (a *analyzer) shortName(pkg string, group bool) string {
|
||||
if a.modulePrefix != "" && gstr.HasPrefix(pkg, a.modulePrefix) {
|
||||
@ -534,7 +927,6 @@ func (a *analyzer) getDependencyStats(_ Input) map[string]any {
|
||||
IncludeInternal: true,
|
||||
IncludeExternal: true,
|
||||
IncludeStdLib: true,
|
||||
MainModuleOnly: false,
|
||||
Depth: 0,
|
||||
}
|
||||
|
||||
@ -639,11 +1031,10 @@ func (a *analyzer) buildPackageStore() *PackageStore {
|
||||
// Convert go packages to PackageInfo
|
||||
for path, goPkg := range a.packages {
|
||||
pkgInfo := &PackageInfo{
|
||||
ImportPath: path,
|
||||
ModulePath: goPkg.Module.Path,
|
||||
IsStdLib: goPkg.Standard,
|
||||
Imports: goPkg.Imports,
|
||||
IsMainModule: a.isMainModulePackage(path),
|
||||
ImportPath: path,
|
||||
ModulePath: goPkg.Module.Path,
|
||||
IsStdLib: goPkg.Standard,
|
||||
Imports: goPkg.Imports,
|
||||
}
|
||||
pkgInfo.Kind = store.identifyPackageKind(pkgInfo)
|
||||
store.packages[path] = pkgInfo
|
||||
|
||||
@ -16,22 +16,46 @@ func TestExternalDependencyAnalysis(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
analyzer := newAnalyzer()
|
||||
analyzer.modulePrefix = "github.com/gogf/gf/cmd/gf/v2"
|
||||
analyzer.packages = map[string]*goPackage{
|
||||
"github.com/other/package": {
|
||||
ImportPath: "github.com/other/package",
|
||||
Standard: false,
|
||||
},
|
||||
"github.com/gogf/gf/cmd/gf/v2/internal": {
|
||||
ImportPath: "github.com/gogf/gf/cmd/gf/v2/internal",
|
||||
Standard: false,
|
||||
},
|
||||
"fmt": {
|
||||
ImportPath: "fmt",
|
||||
Standard: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Test shouldInclude with external dependencies
|
||||
// Test using new FilterOptions system
|
||||
in := Input{
|
||||
Internal: false,
|
||||
External: true,
|
||||
NoStd: true,
|
||||
}
|
||||
|
||||
opts := analyzer.convertInputToFilterOptions(in)
|
||||
opts.Normalize(analyzer.modulePrefix)
|
||||
store := analyzer.buildPackageStore()
|
||||
|
||||
// Test external package (should be included)
|
||||
t.Assert(analyzer.shouldInclude("github.com/other/package", in), true)
|
||||
externalPkg, ok := store.packages["github.com/other/package"]
|
||||
t.Assert(ok, true)
|
||||
t.Assert(opts.ShouldInclude(externalPkg), true)
|
||||
|
||||
// Test internal package (should not be included)
|
||||
t.Assert(analyzer.shouldInclude("github.com/gogf/gf/cmd/gf/v2/internal", in), false)
|
||||
internalPkg, ok := store.packages["github.com/gogf/gf/cmd/gf/v2/internal"]
|
||||
t.Assert(ok, true)
|
||||
t.Assert(opts.ShouldInclude(internalPkg), false)
|
||||
|
||||
// Test standard library (should not be included due to NoStd)
|
||||
t.Assert(analyzer.shouldInclude("fmt", in), false)
|
||||
stdPkg, ok := store.packages["fmt"]
|
||||
t.Assert(ok, true)
|
||||
t.Assert(opts.ShouldInclude(stdPkg), false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -39,11 +63,11 @@ func TestExternalGrouping(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
analyzer := newAnalyzer()
|
||||
|
||||
// Test external group extraction
|
||||
t.Assert(analyzer.getExternalGroup("github.com/user/repo"), "github.com/user")
|
||||
t.Assert(analyzer.getExternalGroup("golang.org/x/tools"), "golang.org")
|
||||
t.Assert(analyzer.getExternalGroup("fmt"), "stdlib")
|
||||
t.Assert(analyzer.getExternalGroup("simple"), "simple")
|
||||
// Test external group extraction using shortName
|
||||
t.Assert(analyzer.shortName("github.com/user/repo", false), "github.com/user/repo")
|
||||
t.Assert(analyzer.shortName("golang.org/x/tools", false), "golang.org/x/tools")
|
||||
t.Assert(analyzer.shortName("fmt", false), "fmt")
|
||||
t.Assert(analyzer.shortName("simple", false), "simple")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -306,7 +306,6 @@ func (a *analyzer) generateJSON(in Input) string {
|
||||
"internal": in.Internal,
|
||||
"external": in.External,
|
||||
"nostd": in.NoStd,
|
||||
"main": in.MainOnly,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(result, "", " ")
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
package cmddep
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@ -177,11 +178,30 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp
|
||||
if e := query.Get("external"); e != "" {
|
||||
in.External = e == "true"
|
||||
}
|
||||
if m := query.Get("main"); m != "" {
|
||||
in.MainOnly = m == "true"
|
||||
if m := query.Get("module"); m != "" {
|
||||
in.Module = m == "true"
|
||||
}
|
||||
if d := query.Get("direct"); d != "" {
|
||||
in.Direct = d == "true"
|
||||
}
|
||||
pkg := query.Get("package")
|
||||
|
||||
// Module-level graph
|
||||
if in.Module {
|
||||
// Ensure module graph is loaded
|
||||
if len(a.modules) == 0 {
|
||||
if err := a.loadModuleGraph(context.Background()); err != nil {
|
||||
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := a.buildModuleGraphData(in)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
return
|
||||
}
|
||||
|
||||
var data *graphData
|
||||
if pkg != "" {
|
||||
data = a.buildPackageGraphData(pkg, in)
|
||||
@ -192,7 +212,7 @@ func (a *analyzer) handleGraphAPI(w http.ResponseWriter, r *http.Request, in Inp
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// handlePackagesAPI returns all packages list with dependency stats.
|
||||
// handlePackagesAPI returns all packages list with dependency stats using new system.
|
||||
func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in Input) {
|
||||
query := r.URL.Query()
|
||||
if i := query.Get("internal"); i != "" {
|
||||
@ -201,31 +221,48 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in
|
||||
if e := query.Get("external"); e != "" {
|
||||
in.External = e == "true"
|
||||
}
|
||||
if m := query.Get("main"); m != "" {
|
||||
in.MainOnly = m == "true"
|
||||
if m := query.Get("module"); m != "" {
|
||||
in.Module = m == "true"
|
||||
}
|
||||
if d := query.Get("direct"); d != "" {
|
||||
in.Direct = d == "true"
|
||||
}
|
||||
|
||||
// Module-level packages
|
||||
if in.Module {
|
||||
a.handleModulesAPI(w, in)
|
||||
return
|
||||
}
|
||||
|
||||
opts := a.convertInputToFilterOptions(in)
|
||||
opts.Normalize(a.modulePrefix)
|
||||
store := a.buildPackageStore()
|
||||
|
||||
// Build reverse dependency map (who uses each package)
|
||||
usedByMap := make(map[string]int)
|
||||
for fullPath, pkg := range a.packages {
|
||||
if !a.shouldInclude(fullPath, in) {
|
||||
for fullPath, pkgInfo := range store.packages {
|
||||
if !opts.ShouldInclude(pkgInfo) {
|
||||
continue
|
||||
}
|
||||
fromShort := a.shortName(fullPath, false)
|
||||
if fromShort == "" {
|
||||
continue
|
||||
}
|
||||
for _, dep := range a.filterDeps(pkg.Imports, in) {
|
||||
shortDep := a.shortName(dep, false)
|
||||
if shortDep != "" {
|
||||
usedByMap[shortDep]++
|
||||
for _, dep := range pkgInfo.Imports {
|
||||
depInfo, ok := store.packages[dep]
|
||||
if ok && opts.ShouldInclude(depInfo) {
|
||||
shortDep := a.shortName(dep, false)
|
||||
if shortDep != "" {
|
||||
usedByMap[shortDep]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packages := make([]packageSummary, 0)
|
||||
for _, pkgPath := range a.getSortedPackages() {
|
||||
if !a.shouldInclude(pkgPath, in) {
|
||||
pkgInfo, ok := store.packages[pkgPath]
|
||||
if !ok || !opts.ShouldInclude(pkgInfo) {
|
||||
continue
|
||||
}
|
||||
shortName := a.shortName(pkgPath, false)
|
||||
@ -235,8 +272,9 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in
|
||||
|
||||
// Count dependencies (filtered)
|
||||
depCount := 0
|
||||
if pkg, ok := a.packages[pkgPath]; ok {
|
||||
for _, dep := range a.filterDeps(pkg.Imports, in) {
|
||||
for _, dep := range pkgInfo.Imports {
|
||||
depInfo, ok := store.packages[dep]
|
||||
if ok && opts.ShouldInclude(depInfo) {
|
||||
shortDep := a.shortName(dep, false)
|
||||
if shortDep != "" {
|
||||
depCount++
|
||||
@ -261,7 +299,7 @@ func (a *analyzer) handlePackagesAPI(w http.ResponseWriter, r *http.Request, in
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// handlePackageAPI returns detailed info for a specific package.
|
||||
// handlePackageAPI returns detailed info for a specific package using new system.
|
||||
func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in Input) {
|
||||
query := r.URL.Query()
|
||||
pkgName := query.Get("name")
|
||||
@ -284,7 +322,16 @@ func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in I
|
||||
return
|
||||
}
|
||||
|
||||
pkg := a.packages[fullPath]
|
||||
opts := a.convertInputToFilterOptions(in)
|
||||
opts.Normalize(a.modulePrefix)
|
||||
store := a.buildPackageStore()
|
||||
|
||||
pkgInfo, ok := store.packages[fullPath]
|
||||
if !ok {
|
||||
http.Error(w, "package not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
info := packageInfo{
|
||||
Name: pkgName,
|
||||
FullPath: fullPath,
|
||||
@ -293,18 +340,21 @@ func (a *analyzer) handlePackageAPI(w http.ResponseWriter, r *http.Request, in I
|
||||
}
|
||||
|
||||
// Get dependencies
|
||||
for _, dep := range a.filterDeps(pkg.Imports, in) {
|
||||
shortName := a.shortName(dep, false)
|
||||
if shortName != "" {
|
||||
info.Dependencies = append(info.Dependencies, shortName)
|
||||
for _, dep := range pkgInfo.Imports {
|
||||
depInfo, ok := store.packages[dep]
|
||||
if ok && opts.ShouldInclude(depInfo) {
|
||||
shortName := a.shortName(dep, false)
|
||||
if shortName != "" {
|
||||
info.Dependencies = append(info.Dependencies, shortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(info.Dependencies)
|
||||
|
||||
// Get reverse dependencies (who uses this package)
|
||||
for path, p := range a.packages {
|
||||
for path, p := range store.packages {
|
||||
for _, dep := range p.Imports {
|
||||
if dep == fullPath {
|
||||
if dep == fullPath && opts.ShouldInclude(p) {
|
||||
shortName := a.shortName(path, false)
|
||||
if shortName != "" {
|
||||
info.UsedBy = append(info.UsedBy, shortName)
|
||||
@ -331,11 +381,30 @@ func (a *analyzer) handleTreeAPI(w http.ResponseWriter, r *http.Request, in Inpu
|
||||
if e := query.Get("external"); e != "" {
|
||||
in.External = e == "true"
|
||||
}
|
||||
if m := query.Get("main"); m != "" {
|
||||
in.MainOnly = m == "true"
|
||||
if m := query.Get("module"); m != "" {
|
||||
in.Module = m == "true"
|
||||
}
|
||||
if d := query.Get("direct"); d != "" {
|
||||
in.Direct = d == "true"
|
||||
}
|
||||
pkg := query.Get("package")
|
||||
|
||||
// Module-level tree
|
||||
if in.Module {
|
||||
// Ensure module graph is loaded
|
||||
if len(a.modules) == 0 {
|
||||
if err := a.loadModuleGraph(context.Background()); err != nil {
|
||||
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
output := a.generateModuleTree(in)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write([]byte(output))
|
||||
return
|
||||
}
|
||||
|
||||
var output string
|
||||
if pkg != "" {
|
||||
output = a.generatePackageTree(pkg, in)
|
||||
@ -356,11 +425,30 @@ func (a *analyzer) handleListAPI(w http.ResponseWriter, r *http.Request, in Inpu
|
||||
if e := query.Get("external"); e != "" {
|
||||
in.External = e == "true"
|
||||
}
|
||||
if m := query.Get("main"); m != "" {
|
||||
in.MainOnly = m == "true"
|
||||
if m := query.Get("module"); m != "" {
|
||||
in.Module = m == "true"
|
||||
}
|
||||
if d := query.Get("direct"); d != "" {
|
||||
in.Direct = d == "true"
|
||||
}
|
||||
pkg := query.Get("package")
|
||||
|
||||
// Module-level list
|
||||
if in.Module {
|
||||
// Ensure module graph is loaded
|
||||
if len(a.modules) == 0 {
|
||||
if err := a.loadModuleGraph(context.Background()); err != nil {
|
||||
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
output := a.generateModuleList(in)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write([]byte(output))
|
||||
return
|
||||
}
|
||||
|
||||
var output string
|
||||
if pkg != "" {
|
||||
output = a.generatePackageList(pkg, in)
|
||||
@ -412,7 +500,7 @@ func (a *analyzer) buildGraphData(in Input) *graphData {
|
||||
return data
|
||||
}
|
||||
|
||||
// buildPackageGraphData builds graph data for a specific package.
|
||||
// buildPackageGraphData builds graph data for a specific package using new system.
|
||||
func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
|
||||
data := &graphData{
|
||||
Nodes: make([]graphNode, 0),
|
||||
@ -432,6 +520,15 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
|
||||
return data
|
||||
}
|
||||
|
||||
opts := a.convertInputToFilterOptions(in)
|
||||
opts.Normalize(a.modulePrefix)
|
||||
store := a.buildPackageStore()
|
||||
|
||||
_, ok := store.packages[fullPath]
|
||||
if !ok {
|
||||
return data
|
||||
}
|
||||
|
||||
nodeSet := make(map[string]bool)
|
||||
nodeSet[pkgName] = true
|
||||
data.Nodes = append(data.Nodes, graphNode{
|
||||
@ -444,9 +541,9 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
|
||||
|
||||
if in.Reverse {
|
||||
// Show packages that depend on this package
|
||||
for path, p := range a.packages {
|
||||
for path, p := range store.packages {
|
||||
for _, dep := range p.Imports {
|
||||
if dep == fullPath {
|
||||
if dep == fullPath && opts.ShouldInclude(p) {
|
||||
shortName := a.shortName(path, false)
|
||||
if shortName != "" && !nodeSet[shortName] {
|
||||
nodeSet[shortName] = true
|
||||
@ -472,14 +569,27 @@ func (a *analyzer) buildPackageGraphData(pkgName string, in Input) *graphData {
|
||||
return data
|
||||
}
|
||||
|
||||
// collectPackageDeps recursively collects dependencies for a package.
|
||||
// collectPackageDeps recursively collects dependencies for a package using new system.
|
||||
func (a *analyzer) collectPackageDeps(pkg *goPackage, pkgName string, in Input, nodeSet map[string]bool, data *graphData, depth int) {
|
||||
if in.Depth > 0 && depth >= in.Depth {
|
||||
return
|
||||
}
|
||||
|
||||
deps := a.filterDeps(pkg.Imports, in)
|
||||
for _, dep := range deps {
|
||||
opts := a.convertInputToFilterOptions(in)
|
||||
opts.Normalize(a.modulePrefix)
|
||||
store := a.buildPackageStore()
|
||||
|
||||
pkgInfo, ok := store.packages[pkg.ImportPath]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, dep := range pkgInfo.Imports {
|
||||
depInfo, ok := store.packages[dep]
|
||||
if !ok || !opts.ShouldInclude(depInfo) {
|
||||
continue
|
||||
}
|
||||
|
||||
shortName := a.shortName(dep, false)
|
||||
if shortName == "" {
|
||||
continue
|
||||
@ -506,7 +616,7 @@ func (a *analyzer) collectPackageDeps(pkg *goPackage, pkgName string, in Input,
|
||||
}
|
||||
}
|
||||
|
||||
// generatePackageTree generates tree output for a specific package.
|
||||
// generatePackageTree generates tree output for a specific package using new system.
|
||||
func (a *analyzer) generatePackageTree(pkgName string, in Input) string {
|
||||
var fullPath string
|
||||
for path := range a.packages {
|
||||
@ -520,15 +630,29 @@ func (a *analyzer) generatePackageTree(pkgName string, in Input) string {
|
||||
return "Package not found: " + pkgName
|
||||
}
|
||||
|
||||
opts := a.convertInputToFilterOptions(in)
|
||||
opts.Normalize(a.modulePrefix)
|
||||
store := a.buildPackageStore()
|
||||
|
||||
_, ok := store.packages[fullPath]
|
||||
if !ok {
|
||||
return "Package not found: " + pkgName
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
pkg := a.packages[fullPath]
|
||||
a.visited = make(map[string]bool)
|
||||
ctx := &TraversalContext{
|
||||
visited: make(map[string]bool),
|
||||
options: opts,
|
||||
store: store,
|
||||
maxDepth: opts.Depth,
|
||||
}
|
||||
|
||||
sb.WriteString(pkgName + "\n")
|
||||
a.printTreeNode(&sb, pkg, "", in, 0)
|
||||
a.printTreeNodeNew(&sb, fullPath, "", in, ctx, 0)
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// generatePackageList generates list output for a specific package.
|
||||
// generatePackageList generates list output for a specific package using new system.
|
||||
func (a *analyzer) generatePackageList(pkgName string, in Input) string {
|
||||
var fullPath string
|
||||
for path := range a.packages {
|
||||
@ -542,15 +666,25 @@ func (a *analyzer) generatePackageList(pkgName string, in Input) string {
|
||||
return "Package not found: " + pkgName
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
pkg := a.packages[fullPath]
|
||||
deps := a.filterDeps(pkg.Imports, in)
|
||||
opts := a.convertInputToFilterOptions(in)
|
||||
opts.Normalize(a.modulePrefix)
|
||||
store := a.buildPackageStore()
|
||||
|
||||
shortDeps := make([]string, 0, len(deps))
|
||||
for _, dep := range deps {
|
||||
shortName := a.shortName(dep, false)
|
||||
if shortName != "" {
|
||||
shortDeps = append(shortDeps, shortName)
|
||||
pkgInfo, ok := store.packages[fullPath]
|
||||
if !ok {
|
||||
return "Package not found: " + pkgName
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
shortDeps := make([]string, 0)
|
||||
for _, dep := range pkgInfo.Imports {
|
||||
depInfo, ok := store.packages[dep]
|
||||
if ok && opts.ShouldInclude(depInfo) {
|
||||
shortName := a.shortName(dep, false)
|
||||
if shortName != "" {
|
||||
shortDeps = append(shortDeps, shortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(shortDeps)
|
||||
@ -743,6 +877,94 @@ func (s *serverState) handleAnalyzeAPI(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// IMPORTANT: Load module dependencies from the actual module directory
|
||||
// We need to run commands in moduleDir context to get the correct direct/indirect info
|
||||
// Running in tempDir would show everything as indirect since we're not using them there
|
||||
|
||||
// IMPORTANT: Load direct dependencies info FIRST
|
||||
// This must be done before parsing go mod graph so that IsDirect flag is correct
|
||||
listModCmd := exec.Command("go", "list", "-m", "-json", "all")
|
||||
listModCmd.Dir = moduleDir // Run in the actual module directory
|
||||
modOutput, err := listModCmd.CombinedOutput()
|
||||
if err == nil {
|
||||
modDecoder := json.NewDecoder(strings.NewReader(string(modOutput)))
|
||||
for modDecoder.More() {
|
||||
var mod struct {
|
||||
Path string `json:"Path"`
|
||||
Version string `json:"Version"`
|
||||
Main bool `json:"Main"`
|
||||
Indirect bool `json:"Indirect"`
|
||||
}
|
||||
if err := modDecoder.Decode(&mod); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip main module
|
||||
if mod.Main {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track direct vs indirect
|
||||
if !mod.Indirect {
|
||||
newAnalyzer.directModules[mod.Path] = true
|
||||
}
|
||||
|
||||
// Create module info
|
||||
newAnalyzer.modules[mod.Path] = &ModuleInfo{
|
||||
Path: mod.Path,
|
||||
Version: mod.Version,
|
||||
IsDirect: !mod.Indirect,
|
||||
Deps: []string{},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load module-level dependencies for the remote module
|
||||
// Run go mod graph in the module directory context
|
||||
graphCmd := exec.Command("go", "mod", "graph")
|
||||
graphCmd.Dir = moduleDir // Run in the actual module directory
|
||||
graphOutput, err := graphCmd.CombinedOutput()
|
||||
if err == nil {
|
||||
// Parse module graph
|
||||
lines := strings.Split(strings.TrimSpace(string(graphOutput)), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
from := newAnalyzer.parseModulePath(parts[0])
|
||||
to := newAnalyzer.parseModulePath(parts[1])
|
||||
|
||||
// Add to graph
|
||||
newAnalyzer.moduleGraph[from] = append(newAnalyzer.moduleGraph[from], to)
|
||||
|
||||
// Ensure both modules exist in our map (use existing or create with correct IsDirect)
|
||||
if _, ok := newAnalyzer.modules[from]; !ok {
|
||||
newAnalyzer.modules[from] = &ModuleInfo{
|
||||
Path: from,
|
||||
Version: newAnalyzer.parseModuleVersion(parts[0]),
|
||||
IsDirect: newAnalyzer.directModules[from],
|
||||
}
|
||||
}
|
||||
if _, ok := newAnalyzer.modules[to]; !ok {
|
||||
newAnalyzer.modules[to] = &ModuleInfo{
|
||||
Path: to,
|
||||
Version: newAnalyzer.parseModuleVersion(parts[1]),
|
||||
IsDirect: newAnalyzer.directModules[to],
|
||||
}
|
||||
}
|
||||
|
||||
// Update deps
|
||||
newAnalyzer.modules[from].Deps = append(newAnalyzer.modules[from].Deps, to)
|
||||
}
|
||||
}
|
||||
|
||||
s.currentAnalyzer = newAnalyzer
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@ -819,3 +1041,105 @@ func (s *serverState) handleResetAPI(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// handleModulesAPI returns module-level dependency list.
|
||||
func (a *analyzer) handleModulesAPI(w http.ResponseWriter, in Input) {
|
||||
// Ensure module graph is loaded
|
||||
if len(a.modules) == 0 {
|
||||
if err := a.loadModuleGraph(context.Background()); err != nil {
|
||||
http.Error(w, "Failed to load module graph: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
modules := a.getFilteredModules(in)
|
||||
|
||||
// Build module list
|
||||
moduleList := make([]map[string]any, 0)
|
||||
for _, mod := range modules {
|
||||
m := map[string]any{
|
||||
"name": mod.Path,
|
||||
"depCount": len(mod.Deps),
|
||||
"usedByCount": 0, // TODO: calculate reverse deps
|
||||
"version": mod.Version,
|
||||
"direct": mod.IsDirect,
|
||||
}
|
||||
moduleList = append(moduleList, m)
|
||||
}
|
||||
|
||||
// Sort by path
|
||||
sort.Slice(moduleList, func(i, j int) bool {
|
||||
return moduleList[i]["name"].(string) < moduleList[j]["name"].(string)
|
||||
})
|
||||
|
||||
// Count stats
|
||||
directCount := 0
|
||||
indirectCount := 0
|
||||
for _, mod := range modules {
|
||||
if mod.IsDirect {
|
||||
directCount++
|
||||
} else {
|
||||
indirectCount++
|
||||
}
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"packages": moduleList,
|
||||
"statistics": map[string]any{
|
||||
"total": len(modules),
|
||||
"direct": directCount,
|
||||
"indirect": indirectCount,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// buildModuleGraphData builds graph data for module-level visualization.
|
||||
func (a *analyzer) buildModuleGraphData(in Input) *graphData {
|
||||
data := &graphData{
|
||||
Nodes: make([]graphNode, 0),
|
||||
Edges: make([]graphEdge, 0),
|
||||
}
|
||||
|
||||
nodeSet := make(map[string]bool)
|
||||
edges := a.collectModuleEdges(in)
|
||||
|
||||
for edge := range edges {
|
||||
parts := strings.Split(edge, " --> ")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
from, to := parts[0], parts[1]
|
||||
|
||||
if !nodeSet[from] {
|
||||
nodeSet[from] = true
|
||||
group := "indirect"
|
||||
if mod, ok := a.modules[from]; ok && mod.IsDirect {
|
||||
group = "direct"
|
||||
}
|
||||
data.Nodes = append(data.Nodes, graphNode{
|
||||
ID: from,
|
||||
Label: strings.ReplaceAll(from, "_", "/"),
|
||||
Group: group,
|
||||
})
|
||||
}
|
||||
if !nodeSet[to] {
|
||||
nodeSet[to] = true
|
||||
group := "indirect"
|
||||
if mod, ok := a.modules[to]; ok && mod.IsDirect {
|
||||
group = "direct"
|
||||
}
|
||||
data.Nodes = append(data.Nodes, graphNode{
|
||||
ID: to,
|
||||
Label: strings.ReplaceAll(to, "_", "/"),
|
||||
Group: group,
|
||||
})
|
||||
}
|
||||
|
||||
data.Edges = append(data.Edges, graphEdge{From: from, To: to})
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@ -13,6 +13,129 @@ import (
|
||||
"github.com/gogf/gf/v2/test/gtest"
|
||||
)
|
||||
|
||||
// Test data model creation and classification
|
||||
func Test_PackageInfo_Creation(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
pkg := &PackageInfo{
|
||||
ImportPath: "github.com/gogf/gf/v2/os/gfile",
|
||||
ModulePath: "github.com/gogf/gf/v2",
|
||||
Kind: KindInternal,
|
||||
Tier: 2,
|
||||
Imports: []string{"fmt", "os"},
|
||||
IsStdLib: false,
|
||||
IsModuleRoot: false,
|
||||
}
|
||||
t.Assert(pkg != nil, true)
|
||||
t.Assert(pkg.Kind, KindInternal)
|
||||
t.Assert(pkg.Tier, 2)
|
||||
t.Assert(len(pkg.Imports), 2)
|
||||
})
|
||||
}
|
||||
|
||||
// Test FilterOptions normalization
|
||||
func Test_FilterOptions_Normalize(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
opts := &FilterOptions{
|
||||
IncludeInternal: false,
|
||||
IncludeExternal: false,
|
||||
IncludeStdLib: false,
|
||||
}
|
||||
opts.Normalize("github.com/gogf/gf/v2")
|
||||
|
||||
// After normalization, internal should be included by default
|
||||
t.Assert(opts.IncludeInternal, true)
|
||||
t.Assert(opts.IncludeExternal, false)
|
||||
})
|
||||
}
|
||||
|
||||
// Test ShouldInclude decision logic
|
||||
func Test_FilterOptions_ShouldInclude(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
// Test internal package inclusion
|
||||
opts := &FilterOptions{
|
||||
IncludeInternal: true,
|
||||
IncludeExternal: false,
|
||||
IncludeStdLib: true,
|
||||
}
|
||||
|
||||
internalPkg := &PackageInfo{
|
||||
Kind: KindInternal,
|
||||
IsStdLib: false,
|
||||
}
|
||||
externalPkg := &PackageInfo{
|
||||
Kind: KindExternal,
|
||||
IsStdLib: false,
|
||||
}
|
||||
stdlibPkg := &PackageInfo{
|
||||
Kind: KindStdLib,
|
||||
IsStdLib: true,
|
||||
}
|
||||
|
||||
t.Assert(opts.ShouldInclude(internalPkg), true)
|
||||
t.Assert(opts.ShouldInclude(externalPkg), false)
|
||||
t.Assert(opts.ShouldInclude(stdlibPkg), true)
|
||||
})
|
||||
}
|
||||
|
||||
// Test TraversalContext Visit tracking
|
||||
func Test_TraversalContext_Visit(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
ctx := &TraversalContext{
|
||||
visited: make(map[string]bool),
|
||||
}
|
||||
|
||||
// First visit should return false
|
||||
t.Assert(ctx.Visit("pkg1"), false)
|
||||
// Second visit should return true
|
||||
t.Assert(ctx.Visit("pkg1"), true)
|
||||
// New package should return false
|
||||
t.Assert(ctx.Visit("pkg2"), false)
|
||||
})
|
||||
}
|
||||
|
||||
// Test guessModuleRoot function
|
||||
func Test_GuessModuleRoot(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{"github.com/gogf/gf", "github.com/gogf/gf"},
|
||||
{"github.com/gogf/gf/v2", "github.com/gogf/gf/v2"},
|
||||
{"github.com/gogf/gf/v2/os/gfile", "github.com/gogf/gf/v2"},
|
||||
{"github.com/gogf/gf/v2/os", "github.com/gogf/gf/v2"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := guessModuleRoot(test.input)
|
||||
t.AssertEQ(result, test.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test input to filter options conversion
|
||||
func Test_ConvertInputToFilterOptions(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
a := newAnalyzer()
|
||||
|
||||
input := Input{
|
||||
Internal: true,
|
||||
External: false,
|
||||
NoStd: true,
|
||||
Module: false,
|
||||
Direct: false,
|
||||
Depth: 3,
|
||||
}
|
||||
|
||||
opts := a.convertInputToFilterOptions(input)
|
||||
|
||||
t.Assert(opts.IncludeInternal, true)
|
||||
t.Assert(opts.IncludeExternal, false)
|
||||
t.Assert(opts.IncludeStdLib, false) // NoStd=true means !IncludeStdLib
|
||||
t.Assert(opts.Depth, 3)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Dep_Tree(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
ctx := gctx.New()
|
||||
@ -112,3 +235,21 @@ func Test_Dep_Group(t *testing.T) {
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ModuleLevel_Direct(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
ctx := gctx.New()
|
||||
|
||||
// Test module level with direct only
|
||||
in := Input{
|
||||
Module: true,
|
||||
Direct: true,
|
||||
Format: "list",
|
||||
}
|
||||
|
||||
t.Logf("Input.Module: %v, Input.Direct: %v", in.Module, in.Direct)
|
||||
|
||||
_, err := Dep.Index(ctx, in)
|
||||
t.AssertNil(err)
|
||||
})
|
||||
}
|
||||
|
||||
@ -123,7 +123,11 @@ async function fetchVersions() {
|
||||
const versionSelect = document.getElementById('versionSelect');
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
|
||||
const modulePath = input.value.trim();
|
||||
let modulePath = input.value.trim();
|
||||
// Remove http:// or https:// prefix if present
|
||||
modulePath = modulePath.replace(/^https?:\/\//, '');
|
||||
input.value = modulePath;
|
||||
|
||||
if (!modulePath) {
|
||||
versionSelect.disabled = true;
|
||||
versionSelect.innerHTML = `<option value="">${i18n.t('selectVersion')}</option>`;
|
||||
@ -163,7 +167,11 @@ async function analyzeRemoteModule() {
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||
|
||||
const modulePath = input.value.trim();
|
||||
let modulePath = input.value.trim();
|
||||
// Remove http:// or https:// prefix if present
|
||||
modulePath = modulePath.replace(/^https?:\/\//, '');
|
||||
input.value = modulePath;
|
||||
|
||||
const version = versionSelect.value;
|
||||
|
||||
if (!modulePath) {
|
||||
@ -410,8 +418,9 @@ async function loadPackages() {
|
||||
try {
|
||||
const internal = document.getElementById('internal').checked;
|
||||
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
|
||||
const mainOnly = document.getElementById('mainOnly') ? document.getElementById('mainOnly').checked : false;
|
||||
const response = await fetch(`/api/packages?internal=${internal}&external=${external}&main=${mainOnly}`);
|
||||
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
|
||||
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
|
||||
const response = await fetch(`/api/packages?internal=${internal}&external=${external}&module=${moduleLevel}&direct=${directOnly}`);
|
||||
const data = await response.json();
|
||||
|
||||
// Handle new API response format with packages and statistics
|
||||
@ -678,7 +687,8 @@ async function refresh() {
|
||||
const reverse = document.getElementById('reverse').checked;
|
||||
const internal = document.getElementById('internal').checked;
|
||||
const external = document.getElementById('external') ? document.getElementById('external').checked : false;
|
||||
const mainOnly = document.getElementById('mainOnly') ? document.getElementById('mainOnly').checked : false;
|
||||
const moduleLevel = document.getElementById('moduleLevel') ? document.getElementById('moduleLevel').checked : false;
|
||||
const directOnly = document.getElementById('directOnly') ? document.getElementById('directOnly').checked : false;
|
||||
|
||||
if (selectedPackage) {
|
||||
await showPackageInfo(selectedPackage);
|
||||
@ -687,11 +697,11 @@ async function refresh() {
|
||||
}
|
||||
|
||||
if (currentView === 'graph') {
|
||||
await refreshGraph(depth, group, reverse, internal, external, mainOnly);
|
||||
await refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly);
|
||||
} else if (currentView === 'tree') {
|
||||
await refreshTree(depth, internal, external, mainOnly);
|
||||
await refreshTree(depth, internal, external, moduleLevel, directOnly);
|
||||
} else {
|
||||
await refreshList(internal, external, mainOnly);
|
||||
await refreshList(internal, external, moduleLevel, directOnly);
|
||||
}
|
||||
}
|
||||
|
||||
@ -724,7 +734,7 @@ async function showPackageInfo(pkg) {
|
||||
}
|
||||
|
||||
// Refresh graph view
|
||||
async function refreshGraph(depth, group, reverse, internal, external, mainOnly) {
|
||||
async function refreshGraph(depth, group, reverse, internal, external, moduleLevel, directOnly) {
|
||||
document.getElementById('graphView').classList.remove('hidden');
|
||||
document.getElementById('textView').classList.add('hidden');
|
||||
|
||||
@ -738,8 +748,11 @@ async function refreshGraph(depth, group, reverse, internal, external, mainOnly)
|
||||
if (external !== undefined) {
|
||||
url += `&external=${external}`;
|
||||
}
|
||||
if (mainOnly !== undefined) {
|
||||
url += `&main=${mainOnly}`;
|
||||
if (moduleLevel !== undefined) {
|
||||
url += `&module=${moduleLevel}`;
|
||||
}
|
||||
if (directOnly !== undefined) {
|
||||
url += `&direct=${directOnly}`;
|
||||
}
|
||||
if (selectedPackage) {
|
||||
url += '&package=' + encodeURIComponent(selectedPackage);
|
||||
@ -815,7 +828,7 @@ function autoFitGraph() {
|
||||
}
|
||||
|
||||
// Refresh tree view
|
||||
async function refreshTree(depth, internal, external, mainOnly) {
|
||||
async function refreshTree(depth, internal, external, moduleLevel, directOnly) {
|
||||
document.getElementById('graphView').classList.add('hidden');
|
||||
document.getElementById('textView').classList.remove('hidden');
|
||||
|
||||
@ -823,8 +836,11 @@ async function refreshTree(depth, internal, external, mainOnly) {
|
||||
if (external !== undefined) {
|
||||
url += `&external=${external}`;
|
||||
}
|
||||
if (mainOnly !== undefined) {
|
||||
url += `&main=${mainOnly}`;
|
||||
if (moduleLevel !== undefined) {
|
||||
url += `&module=${moduleLevel}`;
|
||||
}
|
||||
if (directOnly !== undefined) {
|
||||
url += `&direct=${directOnly}`;
|
||||
}
|
||||
if (selectedPackage) {
|
||||
url += '&package=' + encodeURIComponent(selectedPackage);
|
||||
@ -844,7 +860,7 @@ async function refreshTree(depth, internal, external, mainOnly) {
|
||||
}
|
||||
|
||||
// Refresh list view
|
||||
async function refreshList(internal, external, mainOnly) {
|
||||
async function refreshList(internal, external, moduleLevel, directOnly) {
|
||||
document.getElementById('graphView').classList.add('hidden');
|
||||
document.getElementById('textView').classList.remove('hidden');
|
||||
|
||||
@ -852,8 +868,11 @@ async function refreshList(internal, external, mainOnly) {
|
||||
if (external !== undefined) {
|
||||
url += `&external=${external}`;
|
||||
}
|
||||
if (mainOnly !== undefined) {
|
||||
url += `&main=${mainOnly}`;
|
||||
if (moduleLevel !== undefined) {
|
||||
url += `&module=${moduleLevel}`;
|
||||
}
|
||||
if (directOnly !== undefined) {
|
||||
url += `&direct=${directOnly}`;
|
||||
}
|
||||
if (selectedPackage) {
|
||||
url += '&package=' + encodeURIComponent(selectedPackage);
|
||||
|
||||
@ -24,7 +24,8 @@ const i18n = {
|
||||
groupLabel: 'Group by directory',
|
||||
internalLabel: 'Internal only',
|
||||
externalLabel: 'External packages',
|
||||
mainOnlyLabel: 'Main module only',
|
||||
moduleLevelLabel: 'Module level',
|
||||
directOnlyLabel: 'Direct only',
|
||||
statsInternal: 'Internal',
|
||||
statsExternal: 'External',
|
||||
statsStdlib: 'Stdlib',
|
||||
@ -61,7 +62,8 @@ const i18n = {
|
||||
groupTooltip: 'Group packages by directory structure',
|
||||
internalTooltip: 'Show only internal packages from current module',
|
||||
externalTooltip: 'Include external dependency packages',
|
||||
mainOnlyTooltip: 'Show only main module packages (exclude vendor)',
|
||||
moduleLevelTooltip: 'Show module-level dependencies (from go.mod)',
|
||||
directOnlyTooltip: 'Show only direct dependencies (requires module level)',
|
||||
layoutTDTooltip: 'Top-down layout for dependency graph',
|
||||
layoutLRTooltip: 'Left-right layout for dependency graph',
|
||||
showAllTooltip: 'Clear package selection and show all packages',
|
||||
@ -100,7 +102,8 @@ const i18n = {
|
||||
groupLabel: '按目录分组',
|
||||
internalLabel: '仅内部包',
|
||||
externalLabel: '外部依赖包',
|
||||
mainOnlyLabel: '仅主模块',
|
||||
moduleLevelLabel: '模块级别',
|
||||
directOnlyLabel: '仅直接依赖',
|
||||
statsInternal: '内部',
|
||||
statsExternal: '外部',
|
||||
statsStdlib: '标准库',
|
||||
@ -137,7 +140,8 @@ const i18n = {
|
||||
groupTooltip: '按目录结构分组显示包',
|
||||
internalTooltip: '仅显示当前模块的内部包',
|
||||
externalTooltip: '包含外部依赖包',
|
||||
mainOnlyTooltip: '仅显示主模块包(排除vendor目录)',
|
||||
moduleLevelTooltip: '显示模块级别依赖(来自go.mod)',
|
||||
directOnlyTooltip: '仅显示直接依赖(需要启用模块级别)',
|
||||
layoutTDTooltip: '依赖图从上到下布局',
|
||||
layoutLRTooltip: '依赖图从左到右布局',
|
||||
showAllTooltip: '清除包选择,显示所有包',
|
||||
|
||||
@ -79,8 +79,12 @@
|
||||
<label for="external" data-i18n="externalLabel">External packages</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<input type="checkbox" id="mainOnly" onchange="refresh()" data-i18n-title="mainOnlyTooltip">
|
||||
<label for="mainOnly" data-i18n="mainOnlyLabel">Main module only</label>
|
||||
<input type="checkbox" id="moduleLevel" onchange="refresh()" data-i18n-title="moduleLevelTooltip">
|
||||
<label for="moduleLevel" data-i18n="moduleLevelLabel">Module level</label>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<input type="checkbox" id="directOnly" onchange="refresh()" data-i18n-title="directOnlyTooltip">
|
||||
<label for="directOnly" data-i18n="directOnlyLabel">Direct only</label>
|
||||
</div>
|
||||
<div class="control-group graph-only" id="layoutGroup">
|
||||
<label data-i18n="layoutLabel">Layout:</label>
|
||||
|
||||
Reference in New Issue
Block a user