From 633606d377766182ac5be8c524b19ad866801bd0 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sat, 30 May 2026 13:13:23 +0800 Subject: [PATCH] perf: avoid redundant Abs and Stat on directory walks Use NewChildFile to join paths under an absolute parent without calling filepath.Abs per entry. Use DirEntry.IsDir for type checks so directory entries do not trigger an extra os.Stat. Fixes #13 Fixes #15 --- file_handling.go | 23 +++++++++++++++++++++++ file_handling_test.go | 14 ++++++++++++++ find_replace.go | 17 +++-------------- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/file_handling.go b/file_handling.go index 7c0b1a7..9d132c8 100644 --- a/file_handling.go +++ b/file_handling.go @@ -14,6 +14,7 @@ import ( type File struct { Path string info os.FileInfo + isDir *bool } // NewFile resolves path to an absolute path and wraps it in a *File. It @@ -27,6 +28,16 @@ func NewFile(path string) (*File, error) { return &File{Path: absPath}, nil } +// NewChildFile joins name under an already-absolute parent without calling filepath.Abs again. +func NewChildFile(parent *File, name string, entry os.DirEntry) *File { + child := &File{Path: filepath.Join(parent.Path, name)} + if entry != nil { + isDir := entry.IsDir() + child.isDir = &isDir + } + return child +} + func (f *File) Base() string { return filepath.Base(f.Path) } @@ -35,6 +46,18 @@ func (f *File) Dir() string { return filepath.Dir(f.Path) } +// IsDir reports whether f is a directory, using DirEntry metadata when available. +func (f *File) IsDir() bool { + if f.isDir != nil { + return *f.isDir + } + info, err := f.Info() + if err != nil { + return false + } + return info.IsDir() +} + // Info lazily stats the file and caches the result. It returns an error if // the underlying os.Stat fails. func (f *File) Info() (os.FileInfo, error) { diff --git a/file_handling_test.go b/file_handling_test.go index 91ee538..246aa2e 100644 --- a/file_handling_test.go +++ b/file_handling_test.go @@ -6,6 +6,20 @@ import ( ) // TestNewFile exercises NewFile's path-resolution behavior. + +func TestNewChildFile(t *testing.T) { + tmp := t.TempDir() + parent, err := NewFile(tmp) + if err != nil { + t.Fatal(err) + } + child := NewChildFile(parent, "sub", nil) + want := filepath.Join(parent.Path, "sub") + if child.Path != want { + t.Fatalf("NewChildFile path = %q; want %q", child.Path, want) + } +} + func TestNewFile(t *testing.T) { tmp := t.TempDir() diff --git a/find_replace.go b/find_replace.go index 9bdbe79..16cf8c6 100644 --- a/find_replace.go +++ b/find_replace.go @@ -123,14 +123,8 @@ func (fr *findReplace) WalkDir(f *File) { return } - for _, file := range files { - childPath := filepath.Join(f.Path, file.Name()) - childFile, err := NewFile(childPath) - if err != nil { - log.Print(err) - fr.errs.add(err) - continue - } + for _, entry := range files { + childFile := NewChildFile(f, entry.Name(), entry) wg.Add(1) go func() { defer wg.Done() @@ -151,13 +145,8 @@ func (fr *findReplace) WalkDir(f *File) { // the rename step; the failure is returned so the walker can log it and // continue with siblings. func (fr *findReplace) HandleFile(f *File) error { - info, err := f.Info() - if err != nil { - return err - } - // If file is a directory, recurse immediately (depth-first). - if info.IsDir() { + if f.IsDir() { // Ignore certain directories if f.Base() == ".git" { return nil