Skip to content

Commit 4c291f8

Browse files
authored
Merge pull request #27 from Priyans-hu/feat/modernize-go-patterns
Modernize Go patterns: stdlib strings, context.Context, slog
2 parents f090f4c + 14cbea9 commit 4c291f8

File tree

11 files changed

+523
-389
lines changed

11 files changed

+523
-389
lines changed

CLAUDE.md

Lines changed: 342 additions & 225 deletions
Large diffs are not rendered by default.

cmd/argus/main.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"archive/tar"
55
"archive/zip"
66
"compress/gzip"
7+
"context"
78
"encoding/json"
89
"fmt"
910
"io"
11+
"log/slog"
1012
"net/http"
1113
"os"
1214
"os/signal"
@@ -49,6 +51,21 @@ Argus scans your codebase and generates optimized context files
4951
for AI coding assistants (Claude Code, Cursor, Copilot, etc.).
5052
5153
No more manually writing CLAUDE.md or .cursorrules - Argus sees everything.`,
54+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
55+
setupLogging()
56+
},
57+
}
58+
59+
// setupLogging configures slog based on verbose flag
60+
func setupLogging() {
61+
level := slog.LevelInfo
62+
if verbose {
63+
level = slog.LevelDebug
64+
}
65+
handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
66+
Level: level,
67+
})
68+
slog.SetDefault(slog.New(handler))
5269
}
5370

5471
var initCmd = &cobra.Command{
@@ -229,14 +246,26 @@ func runScan(cmd *cobra.Command, args []string) error {
229246
fmt.Println(" Using parallel detector execution...")
230247
}
231248

249+
// Create context with cancellation support
250+
ctx, cancel := context.WithCancel(context.Background())
251+
defer cancel()
252+
253+
// Handle interrupt signals for graceful cancellation
254+
sigChan := make(chan os.Signal, 1)
255+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
256+
go func() {
257+
<-sigChan
258+
cancel()
259+
}()
260+
232261
// Run analysis (parallel or sequential)
233262
var analysis *types.Analysis
234263
if parallel {
235264
pa := analyzer.NewParallelAnalyzer(absPath, nil)
236-
analysis, err = pa.Analyze()
265+
analysis, err = pa.Analyze(ctx)
237266
} else {
238267
a := analyzer.NewAnalyzer(absPath, nil)
239-
analysis, err = a.Analyze()
268+
analysis, err = a.Analyze(ctx)
240269
}
241270
if err != nil {
242271
return fmt.Errorf("analysis failed: %w", err)
@@ -300,14 +329,26 @@ func runSync(cmd *cobra.Command, args []string) error {
300329
fmt.Println(" Using parallel detector execution...")
301330
}
302331

332+
// Create context with cancellation support
333+
ctx, cancel := context.WithCancel(context.Background())
334+
defer cancel()
335+
336+
// Handle interrupt signals for graceful cancellation
337+
sigChan := make(chan os.Signal, 1)
338+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
339+
go func() {
340+
<-sigChan
341+
cancel()
342+
}()
343+
303344
// Run analysis (parallel or sequential)
304345
var analysis *types.Analysis
305346
if parallel {
306347
pa := analyzer.NewParallelAnalyzer(absPath, nil)
307-
analysis, err = pa.Analyze()
348+
analysis, err = pa.Analyze(ctx)
308349
} else {
309350
a := analyzer.NewAnalyzer(absPath, nil)
310-
analysis, err = a.Analyze()
351+
analysis, err = a.Analyze(ctx)
311352
}
312353
if err != nil {
313354
return fmt.Errorf("analysis failed: %w", err)
@@ -537,12 +578,16 @@ func runWatch(cmd *cobra.Command, args []string) error {
537578
fmt.Println(" Press Ctrl+C to stop")
538579
fmt.Println()
539580

581+
// Create context with cancellation for watch mode
582+
ctx, cancel := context.WithCancel(context.Background())
583+
defer cancel()
584+
540585
// Create incremental analyzer
541586
incAnalyzer := analyzer.NewIncrementalAnalyzer(absPath)
542587

543588
// Do initial full generation
544589
fmt.Println("🔍 Running initial full analysis...")
545-
if err := regenerateWithAnalyzer(absPath, cfg, incAnalyzer, "", true); err != nil {
590+
if err := regenerateWithAnalyzer(ctx, absPath, cfg, incAnalyzer, "", true); err != nil {
546591
fmt.Printf("⚠️ Initial generation failed: %v\n", err)
547592
}
548593

@@ -582,7 +627,7 @@ func runWatch(cmd *cobra.Command, args []string) error {
582627

583628
changedFile := lastChangedFile // Capture for closure
584629
debounceTimer = time.AfterFunc(debounceDelay, func() {
585-
if err := regenerateWithAnalyzer(absPath, cfg, incAnalyzer, changedFile, false); err != nil {
630+
if err := regenerateWithAnalyzer(ctx, absPath, cfg, incAnalyzer, changedFile, false); err != nil {
586631
fmt.Printf("⚠️ Regeneration failed: %v\n", err)
587632
}
588633
})
@@ -600,7 +645,7 @@ func runWatch(cmd *cobra.Command, args []string) error {
600645
}
601646
}
602647

603-
func regenerateWithAnalyzer(absPath string, cfg *config.Config, incAnalyzer *analyzer.IncrementalAnalyzer, changedFile string, isInitial bool) error {
648+
func regenerateWithAnalyzer(ctx context.Context, absPath string, cfg *config.Config, incAnalyzer *analyzer.IncrementalAnalyzer, changedFile string, isInitial bool) error {
604649
var analysis *types.Analysis
605650
var impacts []string
606651
var err error
@@ -609,11 +654,11 @@ func regenerateWithAnalyzer(absPath string, cfg *config.Config, incAnalyzer *ana
609654

610655
if isInitial || changedFile == "" {
611656
// Full analysis for initial run
612-
analysis, err = incAnalyzer.AnalyzeFull()
657+
analysis, err = incAnalyzer.AnalyzeFull(ctx)
613658
impacts = []string{analyzer.ImpactAll}
614659
} else {
615660
// Incremental analysis for file changes
616-
analysis, impacts, err = incAnalyzer.AnalyzeIncremental(changedFile)
661+
analysis, impacts, err = incAnalyzer.AnalyzeIncremental(ctx, changedFile)
617662
}
618663

619664
if err != nil {

internal/analyzer/analyzer.go

Lines changed: 23 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package analyzer
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
7+
"log/slog"
68
"os"
79
"path/filepath"
10+
"strings"
811

912
"github.com/Priyans-hu/argus/internal/detector"
1013
"github.com/Priyans-hu/argus/pkg/types"
@@ -25,7 +28,16 @@ func NewAnalyzer(rootPath string, config *types.Config) *Analyzer {
2528
}
2629

2730
// Analyze performs the full codebase analysis
28-
func (a *Analyzer) Analyze() (*types.Analysis, error) {
31+
func (a *Analyzer) Analyze(ctx context.Context) (*types.Analysis, error) {
32+
// Check for cancellation
33+
select {
34+
case <-ctx.Done():
35+
return nil, ctx.Err()
36+
default:
37+
}
38+
39+
slog.Debug("starting analysis", "rootPath", a.rootPath)
40+
2941
// Get absolute path
3042
absPath, err := filepath.Abs(a.rootPath)
3143
if err != nil {
@@ -37,11 +49,13 @@ func (a *Analyzer) Analyze() (*types.Analysis, error) {
3749

3850
// Walk the file tree
3951
walker := NewWalker(absPath)
40-
files, err := walker.Walk()
52+
files, err := walker.Walk(ctx)
4153
if err != nil {
4254
return nil, fmt.Errorf("failed to walk directory: %w", err)
4355
}
4456

57+
slog.Debug("file walk complete", "fileCount", len(files))
58+
4559
// Initialize analysis
4660
analysis := &types.Analysis{
4761
ProjectName: projectName,
@@ -203,10 +217,10 @@ func (a *Analyzer) detectDependencies(rootPath string) []types.Dependency {
203217
// Try go.mod (simplified)
204218
modPath := filepath.Join(rootPath, "go.mod")
205219
if data, err := os.ReadFile(modPath); err == nil {
206-
lines := splitLines(string(data))
220+
lines := strings.Split(string(data), "\n")
207221
inRequire := false
208222
for _, line := range lines {
209-
line = trimSpace(line)
223+
line = strings.TrimSpace(line)
210224
if line == "require (" {
211225
inRequire = true
212226
continue
@@ -217,11 +231,11 @@ func (a *Analyzer) detectDependencies(rootPath string) []types.Dependency {
217231
}
218232
if inRequire && line != "" {
219233
// Skip indirect dependencies
220-
if contains(line, "// indirect") {
234+
if strings.Contains(line, "// indirect") {
221235
continue
222236
}
223237

224-
parts := splitFields(line)
238+
parts := strings.Fields(line)
225239
if len(parts) >= 2 {
226240
pkgName := parts[0]
227241

@@ -246,83 +260,20 @@ func (a *Analyzer) detectDependencies(rootPath string) []types.Dependency {
246260
// isInternalPackage checks if a package path is internal/vendor
247261
func isInternalPackage(pkg string) bool {
248262
// Skip internal subpackages
249-
if contains(pkg, "/internal/") {
263+
if strings.Contains(pkg, "/internal/") {
250264
return true
251265
}
252266
// Skip service-specific subpackages (AWS SDK pattern)
253-
if contains(pkg, "/service/internal/") {
267+
if strings.Contains(pkg, "/service/internal/") {
254268
return true
255269
}
256270
// Skip feature subpackages
257-
if contains(pkg, "/feature/") {
271+
if strings.Contains(pkg, "/feature/") {
258272
return true
259273
}
260274
return false
261275
}
262276

263-
// contains checks if s contains substr (simple implementation)
264-
func contains(s, substr string) bool {
265-
return len(s) >= len(substr) && findSubstring(s, substr) >= 0
266-
}
267-
268-
func findSubstring(s, substr string) int {
269-
for i := 0; i <= len(s)-len(substr); i++ {
270-
if s[i:i+len(substr)] == substr {
271-
return i
272-
}
273-
}
274-
return -1
275-
}
276-
277-
// Helper functions to avoid importing strings package multiple times
278-
func splitLines(s string) []string {
279-
var lines []string
280-
start := 0
281-
for i := 0; i < len(s); i++ {
282-
if s[i] == '\n' {
283-
lines = append(lines, s[start:i])
284-
start = i + 1
285-
}
286-
}
287-
if start < len(s) {
288-
lines = append(lines, s[start:])
289-
}
290-
return lines
291-
}
292-
293-
func trimSpace(s string) string {
294-
start := 0
295-
end := len(s)
296-
for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\r') {
297-
start++
298-
}
299-
for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\r') {
300-
end--
301-
}
302-
return s[start:end]
303-
}
304-
305-
func splitFields(s string) []string {
306-
var fields []string
307-
start := -1
308-
for i, c := range s {
309-
if c == ' ' || c == '\t' {
310-
if start >= 0 {
311-
fields = append(fields, s[start:i])
312-
start = -1
313-
}
314-
} else {
315-
if start < 0 {
316-
start = i
317-
}
318-
}
319-
}
320-
if start >= 0 {
321-
fields = append(fields, s[start:])
322-
}
323-
return fields
324-
}
325-
326277
// detectPyProjectCommands extracts commands from pyproject.toml
327278
func detectPyProjectCommands(info *detector.PyProjectInfo) []types.Command {
328279
var commands []types.Command

internal/analyzer/incremental.go

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

33
import (
4+
"context"
45
"path/filepath"
56
"strings"
67
"sync"
@@ -43,9 +44,9 @@ func NewIncrementalAnalyzer(rootPath string) *IncrementalAnalyzer {
4344
}
4445

4546
// AnalyzeFull performs a full analysis and caches the result
46-
func (ia *IncrementalAnalyzer) AnalyzeFull() (*types.Analysis, error) {
47+
func (ia *IncrementalAnalyzer) AnalyzeFull(ctx context.Context) (*types.Analysis, error) {
4748
a := NewAnalyzer(ia.rootPath, nil)
48-
analysis, err := a.Analyze()
49+
analysis, err := a.Analyze(ctx)
4950
if err != nil {
5051
return nil, err
5152
}
@@ -59,12 +60,12 @@ func (ia *IncrementalAnalyzer) AnalyzeFull() (*types.Analysis, error) {
5960
}
6061

6162
// AnalyzeIncremental performs incremental analysis based on changed file
62-
func (ia *IncrementalAnalyzer) AnalyzeIncremental(changedFile string) (*types.Analysis, []string, error) {
63+
func (ia *IncrementalAnalyzer) AnalyzeIncremental(ctx context.Context, changedFile string) (*types.Analysis, []string, error) {
6364
// If no cache, do full analysis
6465
ia.cacheMu.RLock()
6566
if ia.cache == nil {
6667
ia.cacheMu.RUnlock()
67-
analysis, err := ia.AnalyzeFull()
68+
analysis, err := ia.AnalyzeFull(ctx)
6869
return analysis, []string{ImpactAll}, err
6970
}
7071
ia.cacheMu.RUnlock()
@@ -75,13 +76,13 @@ func (ia *IncrementalAnalyzer) AnalyzeIncremental(changedFile string) (*types.An
7576
// If all impacts, do full analysis
7677
for _, impact := range impacts {
7778
if impact == ImpactAll {
78-
analysis, err := ia.AnalyzeFull()
79+
analysis, err := ia.AnalyzeFull(ctx)
7980
return analysis, impacts, err
8081
}
8182
}
8283

8384
// Get fresh file list
84-
files, err := ia.walker.Walk()
85+
files, err := ia.walker.Walk(ctx)
8586
if err != nil {
8687
return nil, nil, err
8788
}
@@ -95,7 +96,7 @@ func (ia *IncrementalAnalyzer) AnalyzeIncremental(changedFile string) (*types.An
9596
for _, impact := range impacts {
9697
if err := ia.runDetector(impact, files, analysis); err != nil {
9798
// On error, fall back to full analysis
98-
fullAnalysis, fullErr := ia.AnalyzeFull()
99+
fullAnalysis, fullErr := ia.AnalyzeFull(ctx)
99100
return fullAnalysis, []string{ImpactAll}, fullErr
100101
}
101102
}

0 commit comments

Comments
 (0)