package bakeoff import ( "bufio" "io/fs" "os" "path/filepath" "regexp" "sort" "strings" ) // CodeStats counts non-blank lines by role, plus direct dependencies. type CodeStats struct{ Server, Templates, JS, Tests, Deps int } var ( skipDirs = map[string]bool{"dist": true, "vendor": true, ".cache": true, "node_modules": true, ".git": true, "_build": true, "dist-newstyle": true, "target": true, "zig-out": true, ".zig-cache": true, ".build": true} skipExts = map[string]bool{".css": true, ".md": true, ".sum": true, ".lock": true, ".json": true, ".db": true, ".png": true, ".ico": true, ".svg": true} sourceExts = map[string]bool{".go": true, ".rs": true, ".zig": true, ".ml": true, ".mli": true, ".hs": true, ".swift": true, ".mojo": true, ".🔥": true, ".rb": true, ".nim": true, ".nims": true, ".nimf": true, ".c": true, ".h": true, ".js": true, ".mjs": true, ".html": true, ".tmpl": true, ".erb": true, ".mustache": true, ".eex": true, ".sql": true} templateExt = map[string]bool{".nimf": true, ".html": true, ".tmpl": true, ".erb": true, ".mustache": true, ".eex": true} testName = regexp.MustCompile(`(_test\.|\.test\.|_spec\.|Spec\.hs$|Test\.hs$|Tests\.swift$)`) vanName = regexp.MustCompile(`^van[-.]`) ) // Roles a file can play in CountCode and SourceFiles. const ( roleServer = "server" roleTemplate = "templates" roleJS = "js" roleTest = "tests" roleDeps = "deps" ) // walkCode calls f for every counted file under dir with its slash path // relative to dir and its role. Build output, vendored code, lockfiles, // CSS and docs are skipped. func walkCode(dir string, f func(rel, role string) error) error { return filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error { if err != nil { return err } rel, _ := filepath.Rel(dir, p) rel = filepath.ToSlash(rel) if d.IsDir() { if p != dir && skipDirs[d.Name()] { return filepath.SkipDir } return nil } if rel == "DEPS" { return f(rel, roleDeps) } ext := strings.ToLower(filepath.Ext(p)) if skipExts[ext] || !sourceExts[ext] || vanName.MatchString(d.Name()) || generated(p) { return nil } parts := strings.Split(rel, "/") inDir := func(names ...string) bool { for _, seg := range parts[:len(parts)-1] { for _, n := range names { if seg == n { return true } } } return false } switch { case inDir("test", "tests") || testName.MatchString(d.Name()): return f(rel, roleTest) case ext == ".js" || ext == ".mjs": return f(rel, roleJS) case templateExt[ext] || inDir("templates", "views"): return f(rel, roleTemplate) } return f(rel, roleServer) }) } // CountCode counts non-blank lines under dir by role; DEPS counts as Deps. func CountCode(dir string) (CodeStats, error) { var c CodeStats err := walkCode(dir, func(rel, role string) error { n, err := lines(filepath.Join(dir, rel)) if err != nil { return err } switch role { case roleServer: c.Server += n case roleTemplate: c.Templates += n case roleJS: c.JS += n case roleTest: c.Tests += n case roleDeps: c.Deps = n } return nil }) return c, err } // SourceFiles lists the app's own server, template and JS files (no tests), // sorted: what a reader of the app has to read. func SourceFiles(dir string) ([]string, error) { var out []string err := walkCode(dir, func(rel, role string) error { if role == roleServer || role == roleTemplate || role == roleJS { out = append(out, rel) } return nil }) sort.Strings(out) return out, err } func lines(path string) (int, error) { f, err := os.Open(path) if err != nil { return 0, err } defer f.Close() n := 0 sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 1<<20), 1<<24) for sc.Scan() { if strings.TrimSpace(sc.Text()) != "" { n++ } } return n, sc.Err() } var generatedMark = regexp.MustCompile(`(?i)(\bgenerated\b|do not edit)`) // generated reports whether a file says, in its first three lines, that a // tool wrote it. Such files are copies (embedded assets, say), not code // anyone wrote, so they're neither counted nor shown to the scorer. func generated(path string) bool { f, err := os.Open(path) if err != nil { return false } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 1<<20), 1<<24) for i := 0; i < 3 && sc.Scan(); i++ { if generatedMark.MatchString(sc.Text()) { return true } } return false }