-
Notifications
You must be signed in to change notification settings - Fork 1
dev-cache: add per-project include/exclude and age-based cache filters #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,6 +35,10 @@ var ( | |||||||||||||||||||||||||||||||||||||||||||||||||
| flagScan = flag.String("scan", "", "Directory to scan (overrides config default)") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flagDepth = flag.Int("depth", 0, "Max scan depth (0 = use config default, overrides config)") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flagLangs = flag.String("languages", "", "Comma-separated list of languages to scan") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flagExclude = flag.String("exclude", "", "Comma-separated path patterns to exclude from scan") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flagInclude = flag.String("include-only", "", "Comma-separated path patterns to include only") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flagMinAge = flag.String("min-age", "", "Only include caches older than this (e.g. 30d, 48h)") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flagMaxAge = flag.String("max-age", "", "Only include caches newer than this (e.g. 7d, 24h)") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ----- Config types ----- | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,6 +53,10 @@ type Options struct { | |||||||||||||||||||||||||||||||||||||||||||||||||
| DefaultScanPath string `yaml:"defaultScanPath"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MaxDepth int `yaml:"maxDepth"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| DetectLanguage bool `yaml:"detectLanguage"` // If true, detect language per directory and search only relevant patterns | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ExcludePaths []string `yaml:"excludePaths"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| IncludeOnly []string `yaml:"includeOnly"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MinAge string `yaml:"minAge"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MaxAge string `yaml:"maxAge"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| type Language struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -85,6 +93,14 @@ type Report struct { | |||||||||||||||||||||||||||||||||||||||||||||||||
| Warnings []string `json:"warnings"` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| type ScanFilters struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ExcludePaths []string | ||||||||||||||||||||||||||||||||||||||||||||||||||
| IncludeOnly []string | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MinAge time.Duration | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MaxAge time.Duration | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Now time.Time | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ----- Utilities ----- | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func defaultConfigPath() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -100,6 +116,104 @@ func ensureDir(p string) error { return os.MkdirAll(filepath.Dir(p), 0o755) } | |||||||||||||||||||||||||||||||||||||||||||||||||
| func home() string { h, _ := os.UserHomeDir(); return h } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| func expand(p string) string { return os.ExpandEnv(strings.ReplaceAll(p, "~", home())) } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func splitCSV(v string) []string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.TrimSpace(v) == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parts := strings.Split(v, ",") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out := make([]string, 0, len(parts)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, p := range parts { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| s := strings.TrimSpace(p) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if s != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out = append(out, s) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return out | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func expandAndCleanPatterns(patterns []string) []string { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out := make([]string, 0, len(patterns)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, p := range patterns { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.TrimSpace(p) == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| out = append(out, filepath.Clean(expand(strings.TrimSpace(p)))) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return out | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func matchesPathPattern(path, pattern string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| path = filepath.Clean(path) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pattern = filepath.Clean(pattern) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.ContainsAny(pattern, "*?[") { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ok, err := filepath.Match(pattern, path) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return err == nil && ok | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if path == pattern { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return strings.HasPrefix(path, pattern+string(filepath.Separator)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func matchesAnyPattern(path string, patterns []string) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, p := range patterns { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if matchesPathPattern(path, p) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func parseAgeDuration(s string) (time.Duration, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if strings.TrimSpace(s) == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| s = strings.TrimSpace(strings.ToLower(s)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| unit := s[len(s)-1] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| num := strings.TrimSpace(s[:len(s)-1]) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if num == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0, fmt.Errorf("invalid duration: %q", s) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var mult time.Duration | ||||||||||||||||||||||||||||||||||||||||||||||||||
| switch unit { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'm': | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mult = time.Minute | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'h': | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mult = time.Hour | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'd': | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mult = 24 * time.Hour | ||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'w': | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mult = 7 * 24 * time.Hour | ||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0, fmt.Errorf("unsupported duration unit in %q (use m/h/d/w)", s) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var n int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, err := fmt.Sscanf(num, "%d", &n); err != nil || n < 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0, fmt.Errorf("invalid duration value: %q", s) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return time.Duration(n) * mult, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func passesAgeFilter(f Finding, filters ScanFilters) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if !isCacheDirectory(f) || f.ModMax.IsZero() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| now := filters.Now | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if now.IsZero() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| now = time.Now() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| age := now.Sub(f.ModMax) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if filters.MinAge > 0 && age < filters.MinAge { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if filters.MaxAge > 0 && age > filters.MaxAge { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| func checkVersionFlag() bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, arg := range os.Args[1:] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if arg == "-version" || arg == "--version" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -201,6 +315,10 @@ func writeStarterConfig(path string, force bool) error { | |||||||||||||||||||||||||||||||||||||||||||||||||
| DefaultScanPath: "~/src", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MaxDepth: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| DetectLanguage: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ExcludePaths: []string{}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| IncludeOnly: []string{}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MinAge: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MaxAge: "", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Languages: []Language{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {Name: "node", Enabled: true, Priority: 10, Patterns: []string{"node_modules", ".npm", ".yarn", ".pnpm-store"}, Signatures: []string{"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"}}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -278,6 +396,41 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||
| maxDepth = *flagDepth | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| minAgeStr := cfg.Options.MinAge | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if *flagMinAge != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| minAgeStr = *flagMinAge | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| maxAgeStr := cfg.Options.MaxAge | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if *flagMaxAge != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| maxAgeStr = *flagMaxAge | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| minAge, err := parseAgeDuration(minAgeStr) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Println("invalid --min-age:", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| maxAge, err := parseAgeDuration(maxAgeStr) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Println("invalid --max-age:", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| excludePatterns := cfg.Options.ExcludePaths | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if *flagExclude != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| excludePatterns = splitCSV(*flagExclude) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| includePatterns := cfg.Options.IncludeOnly | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if *flagInclude != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| includePatterns = splitCSV(*flagInclude) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| filters := ScanFilters{ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ExcludePaths: expandAndCleanPatterns(excludePatterns), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| IncludeOnly: expandAndCleanPatterns(includePatterns), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MinAge: minAge, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MaxAge: maxAge, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Now: time.Now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Filter languages | ||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedLangs := map[string]bool{} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if *flagLangs != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -343,7 +496,7 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||
| if cfg.Options.DetectLanguage { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Printf("Language detection enabled - scanning with language-specific patterns\n") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| findings := scanDirectory(scanPath, maxDepth, allPatterns, patternToLang, cfg.Options.DetectLanguage, langSignatures, langPriorities, langToPatterns) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| findings := scanDirectory(scanPath, maxDepth, allPatterns, patternToLang, cfg.Options.DetectLanguage, langSignatures, langPriorities, langToPatterns, filters) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rep.Findings = findings | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| var total int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -424,7 +577,7 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Re-scan to verify | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Println("Re-scanning after cleanup...") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| afterFindings := scanDirectory(scanPath, maxDepth, allPatterns, patternToLang, cfg.Options.DetectLanguage, langSignatures, langPriorities, langToPatterns) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| afterFindings := scanDirectory(scanPath, maxDepth, allPatterns, patternToLang, cfg.Options.DetectLanguage, langSignatures, langPriorities, langToPatterns, filters) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| afterFindings := scanDirectory(scanPath, maxDepth, allPatterns, patternToLang, cfg.Options.DetectLanguage, langSignatures, langPriorities, langToPatterns, filters) | |
| afterFindings := scanDirectory(scanPath, maxDepth, allPatterns, patternToLang, cfg.Options.DetectLanguage, langSignatures, langPriorities, langToPatterns, filters) |
Copilot
AI
Mar 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The include/exclude precedence currently makes ExcludePaths win even when IncludeOnly is set (allowed=true from IncludeOnly, then possibly flipped to false by ExcludePaths). Issue #46's proposed behavior says includeOnly should override exclude (whitelist semantics). Either adjust the logic so IncludeOnly takes precedence over ExcludePaths, or document the intended precedence clearly in README/config docs.
| if allowed && len(filters.ExcludePaths) > 0 && matchesAnyPattern(projectRootPath, filters.ExcludePaths) { | |
| // When IncludeOnly is configured, it has whitelist semantics and | |
| // should not be overridden by ExcludePaths. Only apply ExcludePaths | |
| // when no IncludeOnly patterns are set. | |
| if len(filters.IncludeOnly) == 0 && allowed && len(filters.ExcludePaths) > 0 && matchesAnyPattern(projectRootPath, filters.ExcludePaths) { |
Copilot
AI
Mar 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a directory matches a cache pattern but is excluded by passesAgeFilter, the code uses continue, which means WalkDir will still descend into that cache directory. This can cause a big performance hit (walking huge cache trees) and may produce unexpected nested cache findings under a filtered-out cache dir. Consider returning filepath.SkipDir for matched cache dirs even when they're filtered out by age (and avoid further pattern checks for that directory).
| // Set project root to where language was detected, or parent if no language detection | |
| if detectLang && projectRoot != "" { | |
| f.ProjectRoot = projectRoot | |
| } else { | |
| // If no language detection, use parent directory as project root | |
| f.ProjectRoot = filepath.Dir(cleanPath) | |
| } | |
| if !passesAgeFilter(f, filters) { | |
| continue | |
| } | |
| findings = append(findings, f) | |
| // Set project root to where language was detected, or parent if no language detection | |
| if detectLang && projectRoot != "" { | |
| f.ProjectRoot = projectRoot | |
| } else { | |
| // If no language detection, use parent directory as project root | |
| f.ProjectRoot = filepath.Dir(cleanPath) | |
| } | |
| if !passesAgeFilter(f, filters) { | |
| // Directory matches a cache pattern but is excluded by age; | |
| // skip walking this directory's subtree entirely. | |
| return filepath.SkipDir | |
| } | |
| findings = append(findings, f) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Age filter currently fails open for cache findings with ModMax.IsZero() (returns true). Since inspectPath only updates ModMax based on file mtimes, cache dirs that contain only subdirectories (or unreadable files) may bypass --min-age/--max-age and still be reported/cleaned. Consider initializing ModMax from the directory's own modtime and/or including directory mtimes in inspectPath, or make ModMax.IsZero() fail the age filter when MinAge/MaxAge are set.