diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 0000000..7b359f0 --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,43 @@ +name: Test +description: > + Is this commit good. Nothing is built for release here and nothing is + published; a failure means the code is wrong, not that the pipeline is. + + A composite action rather than a reusable workflow so it runs in the caller's + job, under the caller's name -- CI / Test, Release / Test -- rather than as a + nested "caller / callee" check. + + ONE definition, called by CI and by Release both. This repository had no CI at + all: release.yml was the only workflow, so nothing checked a commit before it + became a tag other repositories download a binary from. + + Assumes Go is already set up; the caller does that, because the toolchain + version is a property of the job rather than of this step. + +runs: + using: composite + steps: + - name: Go formatting + shell: bash + run: | + unformatted="$(gofmt -l .)" + if [ -n "$unformatted" ]; then + echo "::error::these files need gofmt:" + echo "$unformatted" + exit 1 + fi + + - name: Go vet + shell: bash + run: go vet ./... + + # There are no _test.go files today, so this reports "no test files" and + # passes. It is here rather than omitted so that the first test written is + # run by CI the moment it lands, without anyone remembering to wire it up. + - name: Go tests + shell: bash + run: go test ./... + + - name: Go build + shell: bash + run: go build ./... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bfb5518 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +# Every pull request and every merge to main: test, on one runner. +# +# This repository had NO CI. release.yml was the only workflow, so nothing +# checked a commit before it became a tag that other repositories download a +# binary from -- fieldsofrevik's setup action pulls a gdlint release and puts it +# on PATH. +# +# The push trigger is not redundant: there is no CD here, so nothing else covers +# a merge to main. + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + # Bounded, so a step that hangs fails here rather than sitting until the + # runner's own timeout hours later. + timeout-minutes: 20 + steps: + # Third-party actions are pinned by SHA, with the tag in a trailing + # comment so the version is still readable. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version-file: go.mod + + - name: Test + uses: ./.github/actions/test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34fb825..a8bd73d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,14 +20,17 @@ concurrency: release-${{ github.repository }} jobs: release: runs-on: ubuntu-latest + # Bounded, so a step that hangs fails here rather than sitting until the + # runner's own timeout hours later. + timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version-file: go.mod diff --git a/src/core/entity_detector.go b/src/core/entity_detector.go index 5ae08c0..45e6446 100644 --- a/src/core/entity_detector.go +++ b/src/core/entity_detector.go @@ -18,16 +18,16 @@ func NewEntityDetector(file *models.FileInfo) *EntityDetector { func (d *EntityDetector) DetectEntities() { lines := strings.Split(d.file.Content, "\n") d.file.Lines = lines - + var currentEnum string var enumIndent int inEnum := false var lastRPCLine int var className string - + for i, line := range lines { lineNum := i + 1 - + if matches := patterns.ClassNamePattern.FindStringSubmatch(line); len(matches) > 1 { className = matches[1] d.file.ScriptClass = className @@ -40,17 +40,17 @@ func (d *EntityDetector) DetectEntities() { } d.file.AddEntity(entity) } - + if patterns.RPCPattern.MatchString(line) { lastRPCLine = lineNum } - + if matches := patterns.FunctionPattern.FindStringSubmatch(line); len(matches) > 2 { indent := matches[1] funcName := matches[2] isStatic := patterns.StaticFuncPattern.MatchString(line) - isRPC := (lastRPCLine == lineNum - 1) - + isRPC := (lastRPCLine == lineNum-1) + entity := &models.Entity{ Type: models.EntityFunction, Name: funcName, @@ -62,17 +62,17 @@ func (d *EntityDetector) DetectEntities() { IsRPC: isRPC, Parent: className, } - + endLine := d.findFunctionEnd(lines, i, entity.IndentLevel) entity.EndLine = endLine - + d.file.AddEntity(entity) } - + if matches := patterns.SignalPattern.FindStringSubmatch(line); len(matches) > 2 { indent := matches[1] signalName := matches[2] - + entity := &models.Entity{ Type: models.EntitySignal, Name: signalName, @@ -84,16 +84,16 @@ func (d *EntityDetector) DetectEntities() { } d.file.AddEntity(entity) } - + if matches := patterns.ConstantPattern.FindStringSubmatch(line); len(matches) > 2 { indent := matches[1] constName := matches[2] - + value := "" if idx := strings.Index(line, "="); idx > 0 { value = strings.TrimSpace(line[idx+1:]) } - + entity := &models.Entity{ Type: models.EntityConstant, Name: constName, @@ -106,11 +106,11 @@ func (d *EntityDetector) DetectEntities() { } d.file.AddEntity(entity) } - + if matches := patterns.EnumPattern.FindStringSubmatch(line); len(matches) > 2 { indent := matches[1] enumName := matches[2] - + entity := &models.Entity{ Type: models.EntityEnum, Name: enumName, @@ -121,12 +121,12 @@ func (d *EntityDetector) DetectEntities() { Parent: className, } d.file.AddEntity(entity) - + currentEnum = enumName enumIndent = patterns.CountIndentLevel(indent) inEnum = true } - + if inEnum && strings.Contains(line, "}") { lineIndent := patterns.CountIndentLevel(patterns.ExtractIndentation(line)) if lineIndent <= enumIndent { @@ -134,13 +134,13 @@ func (d *EntityDetector) DetectEntities() { currentEnum = "" } } - + if inEnum && currentEnum != "" { trimmed := strings.TrimSpace(line) if trimmed != "" && trimmed != "{" && trimmed != "}" { if matches := patterns.EnumValuePattern.FindStringSubmatch(trimmed); len(matches) > 1 { valueName := matches[1] - + entity := &models.Entity{ Type: models.EntityEnumValue, Name: valueName, @@ -160,18 +160,18 @@ func (d *EntityDetector) DetectEntities() { func (d *EntityDetector) findFunctionEnd(lines []string, startIdx int, funcIndent int) int { for i := startIdx + 1; i < len(lines); i++ { line := lines[i] - + if patterns.IsEmptyOrComment(line) { continue } - + indent := patterns.ExtractIndentation(line) lineIndent := patterns.CountIndentLevel(indent) - + if lineIndent <= funcIndent && !patterns.IsEmptyOrComment(line) { return i } } - + return len(lines) -} \ No newline at end of file +} diff --git a/src/core/indentation_checker.go b/src/core/indentation_checker.go index 60295e2..ebbe203 100644 --- a/src/core/indentation_checker.go +++ b/src/core/indentation_checker.go @@ -16,36 +16,36 @@ func NewIndentationChecker(files []*models.FileInfo) *IndentationChecker { func (c *IndentationChecker) CheckIndentation() []models.CodeLocation { var issues []models.CodeLocation - + for _, file := range c.files { fileIssues := c.checkFile(file) issues = append(issues, fileIssues...) } - + return issues } func (c *IndentationChecker) checkFile(file *models.FileInfo) []models.CodeLocation { var issues []models.CodeLocation - + // GDScript should use tabs for indentation for i, line := range file.Lines { lineNum := i + 1 - + // Skip empty lines if strings.TrimSpace(line) == "" { continue } - + // Get leading whitespace trimmed := strings.TrimLeft(line, " \t") if trimmed == "" { // Line is all whitespace continue } - + indent := line[:len(line)-len(trimmed)] - + // Check for spaces in indentation if strings.Contains(indent, " ") { // Check if it's mixed (both tabs and spaces) @@ -67,7 +67,6 @@ func (c *IndentationChecker) checkFile(file *models.FileInfo) []models.CodeLocat } } } - + return issues } - diff --git a/src/core/pass_analyzer.go b/src/core/pass_analyzer.go index 12668bb..f9ce02e 100644 --- a/src/core/pass_analyzer.go +++ b/src/core/pass_analyzer.go @@ -17,25 +17,25 @@ func NewPassAnalyzer(files []*models.FileInfo) *PassAnalyzer { func (p *PassAnalyzer) FindUnnecessaryPass() []models.CodeLocation { var unnecessary []models.CodeLocation - + for _, file := range p.files { filePass := p.analyzeFile(file) unnecessary = append(unnecessary, filePass...) } - + return unnecessary } func (p *PassAnalyzer) analyzeFile(file *models.FileInfo) []models.CodeLocation { var unnecessary []models.CodeLocation - + for i, line := range file.Lines { lineNum := i + 1 - + if !patterns.PassPattern.MatchString(line) { continue } - + if p.isUnnecessary(file.Lines, i) { unnecessary = append(unnecessary, models.CodeLocation{ File: file.RelativePath, @@ -45,68 +45,68 @@ func (p *PassAnalyzer) analyzeFile(file *models.FileInfo) []models.CodeLocation }) } } - + return unnecessary } func (p *PassAnalyzer) isUnnecessary(lines []string, passIndex int) bool { currentIndent := patterns.CountIndentLevel(patterns.ExtractIndentation(lines[passIndex])) - + hasOtherContent := false for i := passIndex - 1; i >= 0; i-- { line := lines[i] - + if patterns.IsEmptyOrComment(line) { continue } - + lineIndent := patterns.CountIndentLevel(patterns.ExtractIndentation(line)) - + if lineIndent < currentIndent { break } - + if lineIndent == currentIndent && !p.isBlockStart(line) { hasOtherContent = true break } } - + for i := passIndex + 1; i < len(lines); i++ { line := lines[i] - + if patterns.IsEmptyOrComment(line) { continue } - + lineIndent := patterns.CountIndentLevel(patterns.ExtractIndentation(line)) - + if lineIndent < currentIndent { break } - + if lineIndent == currentIndent { hasOtherContent = true break } } - + return hasOtherContent } func (p *PassAnalyzer) isBlockStart(line string) bool { trimmed := strings.TrimSpace(line) - + blockStarters := []string{ "func ", "if ", "elif ", "else:", "for ", "while ", "match ", "class ", "enum ", } - + for _, starter := range blockStarters { if strings.HasPrefix(trimmed, starter) { return true } } - + return strings.HasSuffix(trimmed, ":") -} \ No newline at end of file +} diff --git a/src/models/entity.go b/src/models/entity.go index e865232..2d030d2 100644 --- a/src/models/entity.go +++ b/src/models/entity.go @@ -5,27 +5,27 @@ import "fmt" type EntityType string const ( - EntityFunction EntityType = "function" - EntitySignal EntityType = "signal" - EntityConstant EntityType = "constant" - EntityEnum EntityType = "enum" + EntityFunction EntityType = "function" + EntitySignal EntityType = "signal" + EntityConstant EntityType = "constant" + EntityEnum EntityType = "enum" EntityEnumValue EntityType = "enum_value" EntityClassName EntityType = "class_name" ) type Entity struct { - Type EntityType - Name string - File string - Line int - Column int - EndLine int + Type EntityType + Name string + File string + Line int + Column int + EndLine int IndentLevel int - Parent string - Value string - IsStatic bool - IsRPC bool - IsExported bool + Parent string + Value string + IsStatic bool + IsRPC bool + IsExported bool } func (e *Entity) String() string { @@ -44,36 +44,36 @@ func (e *Entity) IsProtected() bool { if e.Type == EntityClassName { return true } - + if e.Type != EntityFunction { return false } - + protectedPrefixes := []string{ "_ready", "_init", "_enter_tree", "_exit_tree", "_process", "_physics_process", "_input", "_unhandled_input", "_draw", "_gui_input", "_notification", } - + for _, prefix := range protectedPrefixes { if e.Name == prefix { return true } } - + if len(e.Name) >= 4 && e.Name[:4] == "_on_" { return true } - + // Protect getter/setter functions (matches Python linter behavior) if len(e.Name) >= 4 && (e.Name[:4] == "get_" || e.Name[:4] == "set_") { return true } - + if e.IsRPC { return true } - + return false } @@ -106,4 +106,4 @@ func (m EntityMap) Count() int { count += len(entities) } return count -} \ No newline at end of file +} diff --git a/src/operations/file_operations.go b/src/operations/file_operations.go index 0b201d7..0a93940 100644 --- a/src/operations/file_operations.go +++ b/src/operations/file_operations.go @@ -28,7 +28,7 @@ func (f *FileOperations) WriteFile(path string, content string) error { if err := os.MkdirAll(dir, 0755); err != nil { return err } - + return os.WriteFile(path, []byte(content), 0644) } @@ -43,19 +43,19 @@ func (f *FileOperations) FileExists(path string) bool { func (f *FileOperations) GetGDFiles(dir string) ([]string, error) { var files []string - + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - + if !info.IsDir() && strings.HasSuffix(path, ".gd") { files = append(files, path) } - + return nil }) - + return files, err } @@ -72,44 +72,44 @@ func (f *FileOperations) BackupFile(path string) error { if err != nil { return err } - + backupPath := path + ".backup" return f.WriteFile(backupPath, content) } func (f *FileOperations) RestoreFile(path string) error { backupPath := path + ".backup" - + if !f.FileExists(backupPath) { return fmt.Errorf("backup file does not exist: %s", backupPath) } - + content, err := f.ReadFile(backupPath) if err != nil { return err } - + if err := f.WriteFile(path, content); err != nil { return err } - + return f.DeleteFile(backupPath) } func (f *FileOperations) FindFiles(pattern string) ([]string, error) { var matches []string - + err := filepath.Walk(f.rootDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } - + if matched, _ := filepath.Match(pattern, filepath.Base(path)); matched { matches = append(matches, path) } - + return nil }) - + return matches, err -} \ No newline at end of file +} diff --git a/src/operations/remover.go b/src/operations/remover.go index 4aa8910..fdc0abd 100644 --- a/src/operations/remover.go +++ b/src/operations/remover.go @@ -24,7 +24,7 @@ func NewRemover(rootDir string) *Remover { func (r *Remover) RemoveAll(results *models.AnalysisResults) error { filesToModify := make(map[string][]removalTask) - + for _, entity := range results.UnusedFunctions { fullPath := filepath.Join(r.rootDir, entity.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -34,7 +34,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { name: entity.Name, }) } - + for _, entity := range results.UnusedSignals { fullPath := filepath.Join(r.rootDir, entity.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -44,7 +44,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { name: entity.Name, }) } - + for _, entity := range results.UnusedConstants { fullPath := filepath.Join(r.rootDir, entity.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -54,7 +54,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { name: entity.Name, }) } - + for _, entity := range results.UnusedEnums { fullPath := filepath.Join(r.rootDir, entity.File) endLine := r.findEnumEnd(fullPath, entity.Line) @@ -65,7 +65,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { name: entity.Name, }) } - + for _, entity := range results.UnusedClassNames { fullPath := filepath.Join(r.rootDir, entity.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -75,7 +75,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { name: entity.Name, }) } - + for _, location := range results.PrintStatements { fullPath := filepath.Join(r.rootDir, location.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -84,7 +84,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { taskType: "print", }) } - + for _, location := range results.Comments { fullPath := filepath.Join(r.rootDir, location.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -93,7 +93,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { taskType: "comment", }) } - + for _, location := range results.PassStatements { fullPath := filepath.Join(r.rootDir, location.File) filesToModify[fullPath] = append(filesToModify[fullPath], removalTask{ @@ -102,13 +102,13 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { taskType: "pass", }) } - + for filePath, tasks := range filesToModify { if err := r.processFile(filePath, tasks); err != nil { return fmt.Errorf("failed to process %s: %w", filePath, err) } } - + for _, unusedFile := range results.UnusedFiles { fullPath := filepath.Join(r.rootDir, unusedFile) fmt.Printf("Removing unused file: %s\n", unusedFile) @@ -116,7 +116,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { return fmt.Errorf("failed to delete %s: %w", unusedFile, err) } } - + for _, orphanedUID := range results.OrphanedUIDs { fullPath := filepath.Join(r.rootDir, orphanedUID) fmt.Printf("Removing orphaned UID: %s\n", orphanedUID) @@ -124,7 +124,7 @@ func (r *Remover) RemoveAll(results *models.AnalysisResults) error { return fmt.Errorf("failed to delete %s: %w", orphanedUID, err) } } - + return nil } @@ -140,21 +140,21 @@ func (r *Remover) processFile(filePath string, tasks []removalTask) error { if err != nil { return err } - + lines := strings.Split(content, "\n") - + sort.Slice(tasks, func(i, j int) bool { return tasks[i].startLine > tasks[j].startLine }) - + for _, task := range tasks { lines = r.removeLines(lines, task) } - + newContent := strings.Join(lines, "\n") - + cleanContent := r.cleanupEmptyLines(newContent) - + return r.fileOps.WriteFile(filePath, cleanContent) } @@ -162,13 +162,13 @@ func (r *Remover) removeLines(lines []string, task removalTask) []string { if task.startLine <= 0 || task.startLine > len(lines) { return lines } - + startIdx := task.startLine - 1 endIdx := task.endLine if endIdx > len(lines) { endIdx = len(lines) } - + switch task.taskType { case "function": return r.removeFunction(lines, startIdx, endIdx) @@ -196,20 +196,20 @@ func (r *Remover) removeFunction(lines []string, startIdx, endIdx int) []string break } } - + needsPass := r.checkNeedsPass(lines, startIdx, endIdx) - + if needsPass { indent := patterns.ExtractIndentation(lines[startIdx]) replacement := indent + "pass" - + newLines := make([]string, 0, len(lines)-((endIdx-1)-startIdx)) newLines = append(newLines, lines[:startIdx]...) newLines = append(newLines, replacement) newLines = append(newLines, lines[endIdx:]...) return newLines } - + newLines := make([]string, 0, len(lines)-(endIdx-startIdx)) newLines = append(newLines, lines[:startIdx]...) newLines = append(newLines, lines[endIdx:]...) @@ -220,7 +220,7 @@ func (r *Remover) removeSingleLine(lines []string, idx int) []string { if idx < 0 || idx >= len(lines) { return lines } - + newLines := make([]string, 0, len(lines)-1) newLines = append(newLines, lines[:idx]...) newLines = append(newLines, lines[idx+1:]...) @@ -230,7 +230,7 @@ func (r *Remover) removeSingleLine(lines []string, idx int) []string { func (r *Remover) removeEnum(lines []string, startIdx int) []string { endIdx := startIdx + 1 braceCount := 1 - + for i := startIdx + 1; i < len(lines); i++ { line := lines[i] if strings.Contains(line, "{") { @@ -244,7 +244,7 @@ func (r *Remover) removeEnum(lines []string, startIdx int) []string { } } } - + newLines := make([]string, 0, len(lines)-(endIdx-startIdx)) newLines = append(newLines, lines[:startIdx]...) newLines = append(newLines, lines[endIdx:]...) @@ -295,16 +295,16 @@ func (r *Remover) removePrintStatement(lines []string, idx int) []string { func (r *Remover) removeComment(lines []string, idx int) []string { line := lines[idx] - + if patterns.CommentPattern.MatchString(strings.TrimSpace(line)) { return r.removeSingleLine(lines, idx) } - + newLine := patterns.RemoveComments(line) if strings.TrimSpace(newLine) == "" { return r.removeSingleLine(lines, idx) } - + lines[idx] = newLine return lines } @@ -317,40 +317,40 @@ func (r *Remover) checkNeedsPass(lines []string, startIdx, endIdx int) bool { if startIdx == 0 { return false } - + for i := startIdx - 1; i >= 0; i-- { line := lines[i] if patterns.IsEmptyOrComment(line) { continue } - + trimmed := strings.TrimSpace(line) if strings.HasSuffix(trimmed, ":") { hasOtherContent := false baseIndent := patterns.CountIndentLevel(patterns.ExtractIndentation(line)) - + for j := endIdx; j < len(lines); j++ { checkLine := lines[j] if patterns.IsEmptyOrComment(checkLine) { continue } - + checkIndent := patterns.CountIndentLevel(patterns.ExtractIndentation(checkLine)) if checkIndent <= baseIndent { break } - if checkIndent == baseIndent + 1 { + if checkIndent == baseIndent+1 { hasOtherContent = true break } } - + return !hasOtherContent } - + break } - + return false } @@ -359,20 +359,20 @@ func (r *Remover) findEnumEnd(filePath string, startLine int) int { if err != nil { return startLine } - + lines := strings.Split(content, "\n") braceCount := 0 foundStart := false - + for i := startLine - 1; i < len(lines); i++ { line := lines[i] - + if !foundStart && strings.Contains(line, "{") { foundStart = true braceCount = 1 continue } - + if foundStart { if strings.Contains(line, "{") { braceCount++ @@ -385,14 +385,14 @@ func (r *Remover) findEnumEnd(filePath string, startLine int) int { } } } - + return len(lines) } func (r *Remover) cleanupEmptyLines(content string) string { lines := strings.Split(content, "\n") cleaned := make([]string, 0, len(lines)) - + emptyCount := 0 for _, line := range lines { if strings.TrimSpace(line) == "" { @@ -405,10 +405,10 @@ func (r *Remover) cleanupEmptyLines(content string) string { cleaned = append(cleaned, line) } } - + for len(cleaned) > 0 && strings.TrimSpace(cleaned[len(cleaned)-1]) == "" { cleaned = cleaned[:len(cleaned)-1] } - + return strings.Join(cleaned, "\n") -} \ No newline at end of file +} diff --git a/src/optimization/cache.go b/src/optimization/cache.go index b5ac5fd..37f4194 100644 --- a/src/optimization/cache.go +++ b/src/optimization/cache.go @@ -10,10 +10,10 @@ import ( ) type CacheEntry struct { - Content string - ModTime time.Time - FileSize int64 - Hash string + Content string + ModTime time.Time + FileSize int64 + Hash string } type Cache struct { @@ -32,24 +32,24 @@ func NewCache() *Cache { func (c *Cache) Get(path string) (string, bool) { c.mu.RLock() defer c.mu.RUnlock() - + entry, exists := c.entries[path] if !exists { c.misses.Add(1) return "", false } - + info, err := os.Stat(path) if err != nil { c.misses.Add(1) return "", false } - + if info.ModTime().After(entry.ModTime) || info.Size() != entry.FileSize { c.misses.Add(1) return "", false } - + c.hits.Add(1) return entry.Content, true } @@ -59,12 +59,12 @@ func (c *Cache) Set(path string, content string) { if err != nil { return } - + hash := c.computeHash(content) - + c.mu.Lock() defer c.mu.Unlock() - + c.entries[path] = &CacheEntry{ Content: content, ModTime: info.ModTime(), @@ -76,14 +76,14 @@ func (c *Cache) Set(path string, content string) { func (c *Cache) Invalidate(path string) { c.mu.Lock() defer c.mu.Unlock() - + delete(c.entries, path) } func (c *Cache) Clear() { c.mu.Lock() defer c.mu.Unlock() - + c.entries = make(map[string]*CacheEntry) c.hits.Store(0) c.misses.Store(0) @@ -93,18 +93,18 @@ func (c *Cache) Stats() (hits, misses int, hitRate float64) { hits = int(c.hits.Load()) misses = int(c.misses.Load()) total := hits + misses - + if total > 0 { hitRate = float64(hits) / float64(total) * 100 } - + return } func (c *Cache) Size() int { c.mu.RLock() defer c.mu.RUnlock() - + return len(c.entries) } @@ -117,12 +117,12 @@ func (c *Cache) computeHash(content string) string { func (c *Cache) HasChanged(path string, content string) bool { c.mu.RLock() defer c.mu.RUnlock() - + entry, exists := c.entries[path] if !exists { return true } - + newHash := c.computeHash(content) return entry.Hash != newHash -} \ No newline at end of file +} diff --git a/src/optimization/parallel.go b/src/optimization/parallel.go index f996048..7a1276f 100644 --- a/src/optimization/parallel.go +++ b/src/optimization/parallel.go @@ -27,7 +27,7 @@ func NewWorkerPool(workers int) *WorkerPool { if workers <= 0 { workers = runtime.NumCPU() } - + return &WorkerPool{ workers: workers, workQueue: make(chan WorkItem, workers*2), @@ -52,7 +52,7 @@ func (p *WorkerPool) Submit(item WorkItem) { func (p *WorkerPool) worker() { defer p.wg.Done() - + for item := range p.workQueue { item.Func(item.Data) } @@ -60,12 +60,12 @@ func (p *WorkerPool) worker() { func (p *WorkerPool) ProcessBatch(items []WorkItem, results chan<- WorkResult) { var wg sync.WaitGroup - + for _, item := range items { wg.Add(1) go func(wi WorkItem) { defer wg.Done() - + result := wi.Func(wi.Data) results <- WorkResult{ ID: wi.ID, @@ -74,7 +74,7 @@ func (p *WorkerPool) ProcessBatch(items []WorkItem, results chan<- WorkResult) { } }(item) } - + go func() { wg.Wait() close(results) @@ -89,7 +89,7 @@ func NewParallelProcessor(maxWorkers int) *ParallelProcessor { if maxWorkers <= 0 { maxWorkers = runtime.NumCPU() } - + return &ParallelProcessor{ maxWorkers: maxWorkers, } @@ -99,19 +99,19 @@ func (p *ParallelProcessor) Process(items []interface{}, fn func(interface{}) in if len(items) == 0 { return []interface{}{} } - + if len(items) == 1 { return []interface{}{fn(items[0])} } - + numWorkers := p.maxWorkers if len(items) < numWorkers { numWorkers = len(items) } - + resultsChan := make(chan WorkResult, len(items)) results := make([]interface{}, len(items)) - + workItems := make([]WorkItem, len(items)) for i, item := range items { workItems[i] = WorkItem{ @@ -120,16 +120,16 @@ func (p *ParallelProcessor) Process(items []interface{}, fn func(interface{}) in Func: fn, } } - + pool := NewWorkerPool(numWorkers) pool.Start() pool.ProcessBatch(workItems, resultsChan) pool.Stop() - + for result := range resultsChan { results[result.ID] = result.Result } - + return results } @@ -137,25 +137,25 @@ func (p *ParallelProcessor) Map(items []string, fn func(string) string) []string if len(items) == 0 { return []string{} } - + interfaces := make([]interface{}, len(items)) for i, item := range items { interfaces[i] = item } - + wrappedFn := func(item interface{}) interface{} { return fn(item.(string)) } - + results := p.Process(interfaces, wrappedFn) - + strings := make([]string, len(results)) for i, result := range results { if result != nil { strings[i] = result.(string) } } - + return strings } @@ -163,17 +163,17 @@ func (p *ParallelProcessor) Filter(items []string, fn func(string) bool) []strin if len(items) == 0 { return []string{} } - + type filterResult struct { item string keep bool } - + interfaces := make([]interface{}, len(items)) for i, item := range items { interfaces[i] = item } - + wrappedFn := func(item interface{}) interface{} { str := item.(string) return filterResult{ @@ -181,9 +181,9 @@ func (p *ParallelProcessor) Filter(items []string, fn func(string) bool) []strin keep: fn(str), } } - + results := p.Process(interfaces, wrappedFn) - + var filtered []string for _, result := range results { if result != nil { @@ -193,6 +193,6 @@ func (p *ParallelProcessor) Filter(items []string, fn func(string) bool) []strin } } } - + return filtered -} \ No newline at end of file +}