Skip to content

Commit 5570e22

Browse files
committed
feat: improve analyze command
1 parent bc0bbb3 commit 5570e22

File tree

6 files changed

+630
-66
lines changed

6 files changed

+630
-66
lines changed

cmd/gograph/commands/analyze.go

Lines changed: 139 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"sync"
78
"time"
89

@@ -15,6 +16,7 @@ import (
1516
"github.com/compozy/gograph/pkg/errors"
1617
"github.com/compozy/gograph/pkg/logger"
1718
"github.com/compozy/gograph/pkg/progress"
19+
"github.com/mattn/go-isatty"
1820
"github.com/spf13/cobra"
1921
)
2022

@@ -116,6 +118,14 @@ The resulting graph allows you to:
116118
return runAnalysisWithoutProgress(projectPath, projectID, parserConfig, analyzerConfig, neo4jConfig)
117119
}
118120

121+
// Check if we're in TTY mode and suppress logging if so
122+
isTTY := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
123+
if isTTY {
124+
// Suppress all logging output to avoid conflicts with TUI
125+
logger.Disable()
126+
defer logger.Enable() // Re-enable after completion
127+
}
128+
119129
return runAnalysisWithProgress(projectPath, projectID, parserConfig, analyzerConfig, neo4jConfig)
120130
})
121131
},
@@ -206,89 +216,156 @@ func runAnalysisWithProgress(
206216
neo4jConfig *infra.Neo4jConfig,
207217
) error {
208218
ctx := context.Background()
209-
startTime := time.Now()
210219

211-
var parseResult *parser.ParseResult
212-
var report *analyzer.AnalysisReport
213-
var graphResult *core.AnalysisResult
220+
// Initialize adaptive progress
221+
progressIndicator := progress.NewAdaptiveProgress(os.Stdout)
222+
phases := []progress.PhaseInfo{
223+
{Name: "Parsing", Description: "Scanning and parsing Go source files", Weight: 0.3},
224+
{Name: "Analysis", Description: "Analyzing code structure and relationships", Weight: 0.3},
225+
{Name: "Graph Building", Description: "Building graph representation", Weight: 0.2},
226+
{Name: "Storage", Description: "Storing results in Neo4j", Weight: 0.2},
227+
}
228+
progressIndicator.SetPhases(phases)
229+
progressIndicator.Start(fmt.Sprintf("Analyzing project: %s", projectPath))
214230

215-
// -----
216-
// Parsing Phase
217-
// -----
218-
err := progress.WithProgress("Parsing project files", func() error {
219-
parserService := parser.NewService(parserConfig)
220-
var err error
221-
parseResult, err = parserService.ParseProject(ctx, projectPath, parserConfig)
222-
return err
223-
})
231+
// Parse project
232+
parseResult, err := runParsingPhase(ctx, projectPath, parserConfig, progressIndicator)
224233
if err != nil {
225-
return fmt.Errorf("failed to parse project: %w", err)
234+
return err
226235
}
227236

228-
// -----
229-
// Analysis Phase
230-
// -----
231-
err = progress.WithProgress("Analyzing code structure", func() error {
232-
analyzerService := analyzer.NewAnalyzer(analyzerConfig)
233-
analysisInput := &analyzer.AnalysisInput{
234-
ProjectID: projectID.String(),
235-
Files: parseResult.Files,
236-
}
237-
var err error
238-
report, err = analyzerService.AnalyzeProject(ctx, analysisInput)
239-
return err
240-
})
237+
// Analyze project
238+
report, err := runAnalysisPhase(ctx, projectID, parseResult, analyzerConfig, progressIndicator)
241239
if err != nil {
242-
return fmt.Errorf("failed to analyze project: %w", err)
240+
return err
243241
}
244242

245-
// -----
246-
// Graph Building Phase
247-
// -----
248-
err = progress.WithProgress("Building graph representation", func() error {
249-
builder := graph.NewBuilder(nil) // Use default config
250-
var err error
251-
graphResult, err = builder.BuildFromAnalysis(ctx, projectID, parseResult, report)
252-
return err
253-
})
243+
// Build graph
244+
graphResult, err := runGraphBuildingPhase(ctx, projectID, parseResult, report, progressIndicator)
254245
if err != nil {
255-
return fmt.Errorf("failed to build graph: %w", err)
246+
return err
256247
}
257248

258-
// -----
259-
// Storage Phase
260-
// -----
261-
err = progress.WithProgressSteps("Storing in Neo4j", func(update func(string), _ func(int, int)) error {
262-
update("Connecting to database")
263-
repo, err := infra.NewNeo4jRepository(neo4jConfig)
264-
if err != nil {
265-
return fmt.Errorf("failed to create Neo4j repository: %w", err)
266-
}
267-
defer repo.Close()
268-
269-
update("Storing nodes and relationships")
270-
return repo.StoreAnalysis(ctx, graphResult)
271-
})
249+
// Store results
250+
err = runStoragePhase(ctx, graphResult, neo4jConfig, progressIndicator)
272251
if err != nil {
273-
return fmt.Errorf("failed to store analysis: %w", err)
252+
return err
274253
}
275254

276-
duration := time.Since(startTime)
255+
// Success with detailed statistics
256+
successMsg := "Analysis completed successfully!"
257+
258+
// Create detailed statistics
259+
stats := progress.AnalysisStats{
260+
Files: len(parseResult.Files),
261+
Nodes: len(graphResult.Nodes),
262+
Relationships: len(graphResult.Relationships),
263+
Interfaces: len(report.InterfaceImplementations),
264+
CallChains: len(report.CallChains),
265+
ProjectID: projectID.String(),
266+
}
277267

278-
// Final summary
279-
logger.Info("✓ Analysis completed successfully")
280-
logger.Info("Summary:",
281-
"files", len(parseResult.Files),
282-
"nodes", len(graphResult.Nodes),
283-
"relationships", len(graphResult.Relationships),
284-
"duration", duration.Round(time.Millisecond),
285-
"project_id", projectID)
268+
progressIndicator.SuccessWithStats(successMsg, stats)
286269

287270
return nil
288271
}
289272

290273
var initAnalyzeOnce sync.Once
291274

275+
func runParsingPhase(
276+
ctx context.Context,
277+
projectPath string,
278+
parserConfig *parser.Config,
279+
progressIndicator *progress.AdaptiveProgress,
280+
) (*parser.ParseResult, error) {
281+
progressIndicator.UpdatePhase("Parsing")
282+
progressIndicator.UpdateProgress(0.0, "Initializing parser")
283+
284+
parserService := parser.NewService(parserConfig)
285+
progressIndicator.UpdateProgress(0.1, "Scanning project files")
286+
287+
parseResult, err := parserService.ParseProject(ctx, projectPath, parserConfig)
288+
if err != nil {
289+
progressIndicator.Error(fmt.Errorf("failed to parse project: %w", err))
290+
return nil, fmt.Errorf("failed to parse project: %w", err)
291+
}
292+
progressIndicator.UpdateProgress(0.25, fmt.Sprintf("Parsed %d files", len(parseResult.Files)))
293+
return parseResult, nil
294+
}
295+
296+
func runAnalysisPhase(
297+
ctx context.Context,
298+
projectID core.ID,
299+
parseResult *parser.ParseResult,
300+
analyzerConfig *analyzer.Config,
301+
progressIndicator *progress.AdaptiveProgress,
302+
) (*analyzer.AnalysisReport, error) {
303+
progressIndicator.UpdatePhase("Analysis")
304+
progressIndicator.UpdateProgress(0.3, "Initializing analyzer")
305+
306+
analyzerService := analyzer.NewAnalyzer(analyzerConfig)
307+
analysisInput := &analyzer.AnalysisInput{
308+
ProjectID: projectID.String(),
309+
Files: parseResult.Files,
310+
}
311+
progressIndicator.UpdateProgress(0.4, "Analyzing structure and dependencies")
312+
313+
report, err := analyzerService.AnalyzeProject(ctx, analysisInput)
314+
if err != nil {
315+
progressIndicator.Error(fmt.Errorf("failed to analyze project: %w", err))
316+
return nil, fmt.Errorf("failed to analyze project: %w", err)
317+
}
318+
progressIndicator.UpdateProgress(0.55, fmt.Sprintf("Found %d interfaces, %d call chains",
319+
len(report.InterfaceImplementations), len(report.CallChains)))
320+
return report, nil
321+
}
322+
323+
func runGraphBuildingPhase(
324+
ctx context.Context,
325+
projectID core.ID,
326+
parseResult *parser.ParseResult,
327+
report *analyzer.AnalysisReport,
328+
progressIndicator *progress.AdaptiveProgress,
329+
) (*core.AnalysisResult, error) {
330+
progressIndicator.UpdatePhase("Graph Building")
331+
progressIndicator.UpdateProgress(0.6, "Building graph nodes and relationships")
332+
333+
builder := graph.NewBuilder(nil) // Use default config
334+
graphResult, err := builder.BuildFromAnalysis(ctx, projectID, parseResult, report)
335+
if err != nil {
336+
progressIndicator.Error(fmt.Errorf("failed to build graph: %w", err))
337+
return nil, fmt.Errorf("failed to build graph: %w", err)
338+
}
339+
progressIndicator.UpdateProgress(0.75, fmt.Sprintf("Built %d nodes, %d relationships",
340+
len(graphResult.Nodes), len(graphResult.Relationships)))
341+
return graphResult, nil
342+
}
343+
344+
func runStoragePhase(
345+
ctx context.Context,
346+
graphResult *core.AnalysisResult,
347+
neo4jConfig *infra.Neo4jConfig,
348+
progressIndicator *progress.AdaptiveProgress,
349+
) error {
350+
progressIndicator.UpdatePhase("Storage")
351+
progressIndicator.UpdateProgress(0.8, "Connecting to Neo4j database")
352+
353+
repo, err := infra.NewNeo4jRepository(neo4jConfig)
354+
if err != nil {
355+
progressIndicator.Error(fmt.Errorf("failed to create Neo4j repository: %w", err))
356+
return fmt.Errorf("failed to create Neo4j repository: %w", err)
357+
}
358+
defer repo.Close()
359+
360+
progressIndicator.UpdateProgress(0.9, "Storing nodes and relationships")
361+
err = repo.StoreAnalysis(ctx, graphResult)
362+
if err != nil {
363+
progressIndicator.Error(fmt.Errorf("failed to store analysis: %w", err))
364+
return fmt.Errorf("failed to store analysis: %w", err)
365+
}
366+
return nil
367+
}
368+
292369
// InitAnalyzeCommand registers the analyze command
293370
func InitAnalyzeCommand() {
294371
initAnalyzeOnce.Do(func() {

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ toolchain go1.24.4
77
require (
88
github.com/avast/retry-go/v4 v4.6.1
99
github.com/charmbracelet/bubbles v0.21.0
10-
github.com/charmbracelet/bubbletea v1.3.4
10+
github.com/charmbracelet/bubbletea v1.3.5
1111
github.com/charmbracelet/lipgloss v1.1.0
1212
github.com/charmbracelet/log v0.3.1
1313
github.com/google/uuid v1.6.0
1414
github.com/mark3labs/mcp-go v0.32.0
15+
github.com/mattn/go-isatty v0.0.20
1516
github.com/neo4j/neo4j-go-driver/v5 v5.28.1
1617
github.com/sashabaranov/go-openai v1.40.3
1718
github.com/spf13/cobra v1.9.1
@@ -24,6 +25,7 @@ require (
2425
require (
2526
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2627
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
28+
github.com/charmbracelet/harmonica v0.2.0 // indirect
2729
github.com/charmbracelet/x/ansi v0.8.0 // indirect
2830
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
2931
github.com/charmbracelet/x/term v0.2.1 // indirect
@@ -35,7 +37,6 @@ require (
3537
github.com/google/go-cmp v0.7.0 // indirect
3638
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3739
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
38-
github.com/mattn/go-isatty v0.0.20 // indirect
3940
github.com/mattn/go-localereader v0.0.1 // indirect
4041
github.com/mattn/go-runewidth v0.0.16 // indirect
4142
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u
66
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
77
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
88
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
9+
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
10+
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
911
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
1012
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
13+
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
14+
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
1115
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
1216
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
1317
github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw=

pkg/config/config.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,23 @@ func Load(configPath string) (*Config, error) {
7676
cfg := DefaultConfig()
7777

7878
if configPath == "" {
79-
// Look for config file in current directory
80-
configPath = filepath.Join(".", defaultConfigFileName+"."+defaultConfigType)
79+
// Look for config file in current directory - try both formats
80+
possiblePaths := []string{
81+
filepath.Join(".", "gograph.yaml"), // New format
82+
filepath.Join(".", defaultConfigFileName+"."+defaultConfigType), // Legacy format (.gograph.yaml)
83+
}
84+
85+
configPath = ""
86+
for _, path := range possiblePaths {
87+
if _, err := os.Stat(path); err == nil {
88+
configPath = path
89+
break
90+
}
91+
}
92+
93+
if configPath == "" {
94+
return cfg, nil // Return default config if no config file found
95+
}
8196
}
8297

8398
// Check if config file exists

pkg/logger/logger.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logger
22

33
import (
4+
"io"
45
"os"
56
"sync"
67

@@ -80,3 +81,15 @@ func With(keyvals ...any) *log.Logger {
8081
ensureInitialized()
8182
return logger.With(keyvals...)
8283
}
84+
85+
// Disable completely disables logging output
86+
func Disable() {
87+
ensureInitialized()
88+
logger.SetOutput(io.Discard)
89+
}
90+
91+
// Enable re-enables logging output to stderr
92+
func Enable() {
93+
ensureInitialized()
94+
logger.SetOutput(os.Stderr)
95+
}

0 commit comments

Comments
 (0)