Skip to content

add in skip handler - #20

Merged
boyter merged 2 commits into
masterfrom
skipprocessing
Feb 26, 2026
Merged

add in skip handler#20
boyter merged 2 commits into
masterfrom
skipprocessing

Conversation

@boyter

@boyter boyter commented Feb 26, 2026

Copy link
Copy Markdown
Owner

No description provided.

@boyter
boyter requested a review from Copilot February 26, 2026 06:44
@pr-insights pr-insights Bot added VH/complexity Very high complexity XL/size Extra large change labels Feb 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds a skip handler feature to the gocodewalker library, allowing users to register a callback function that is invoked whenever a file or directory is skipped during the walking process. The skip handler receives the path, name, directory flag, and reason for the skip, enabling users to track or log which files/directories are being filtered out and why.

Changes:

  • Added a new SkipReason type with 15 constants representing different skip reasons (gitignore, binary, hidden, extension filters, etc.)
  • Added skipHandler field to FileWalker and SetSkipHandler() method to register custom skip handlers with a default no-op implementation
  • Integrated skip reason tracking and handler invocation throughout the file filtering logic in walkDirectoryRecursive()
  • Added comprehensive test coverage with 5 new test functions covering various skip scenarios for both files and directories
  • Updated documentation in README.md with examples of skip handler usage
  • Added skip handler example to the main.go demo program

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 18 comments.

File Description
file.go Implements skip handler infrastructure: defines SkipReason type and constants, adds skipHandler field and SetSkipHandler method, instruments all skip logic to track reasons and invoke handler
file_test.go Adds comprehensive test suite with 368 lines covering default no-op behavior, nil handler, file skip reasons, and directory skip reasons
cmd/gocodewalker/main.go Demonstrates skip handler usage in example program and changes walker paths from specific directories to current directory
README.md Documents skip handler feature with usage example

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread file_test.go
Comment on lines +938 to +1122
func TestSkipHandlerFileCases(t *testing.T) {
type skipRecord struct {
path string
name string
isDir bool
reason SkipReason
}

type testcase struct {
Name string
Setup func() (*FileWalker, chan *File)
ExpectedSkips int
ExpectedReason SkipReason
ExpectedIsDir bool
}

testCases := []testcase{
{
Name: "ExcludeFilename skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "excluded.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.ExcludeFilename = []string{"excluded.txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonExcludeFilename,
ExpectedIsDir: false,
},
{
Name: "IncludeFilename skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "other.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.IncludeFilename = []string{"wanted.txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonIncludeFilename,
ExpectedIsDir: false,
},
{
Name: "ExcludeFilenameRegex skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.log"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.ExcludeFilenameRegex = []*regexp.Regexp{regexp.MustCompile(`\.log$`)}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonExcludeFilenameRegex,
ExpectedIsDir: false,
},
{
Name: "IncludeFilenameRegex skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.IncludeFilenameRegex = []*regexp.Regexp{regexp.MustCompile(`\.go$`)}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonIncludeFilenameRegex,
ExpectedIsDir: false,
},
{
Name: "AllowListExtension skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.md"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.AllowListExtensions = []string{"go"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonAllowListExtension,
ExpectedIsDir: false,
},
{
Name: "ExcludeListExtension skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.ExcludeListExtensions = []string{"txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonExcludeListExtension,
ExpectedIsDir: false,
},
{
Name: "LocationExcludePattern file skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.LocationExcludePattern = []string{"test.txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonLocationExcludePattern,
ExpectedIsDir: false,
},
{
Name: "Binary file skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_ = os.WriteFile(filepath.Join(d, "binary.bin"), []byte{0}, 0644)

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.IgnoreBinaryFiles = true
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonBinary,
ExpectedIsDir: false,
},
{
Name: "CustomIgnorePatterns file skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.md"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.CustomIgnorePatterns = []string{"*.md"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonCustomIgnore,
ExpectedIsDir: false,
},
}

for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
walker, fileListQueue := tc.Setup()

var skips []skipRecord
walker.SetSkipHandler(func(path string, name string, isDir bool, reason SkipReason) {
skips = append(skips, skipRecord{path: path, name: name, isDir: isDir, reason: reason})
})
walker.osReadFile = func(name string) ([]byte, error) { return nil, nil }
_ = walker.Start()

// drain channel
for range fileListQueue {
}

if len(skips) != tc.ExpectedSkips {
t.Errorf("expected %d skips but got %d", tc.ExpectedSkips, len(skips))
return
}

if len(skips) > 0 {
if skips[0].reason != tc.ExpectedReason {
t.Errorf("expected reason %q but got %q", tc.ExpectedReason, skips[0].reason)
}
if skips[0].isDir != tc.ExpectedIsDir {
t.Errorf("expected isDir=%v but got isDir=%v", tc.ExpectedIsDir, skips[0].isDir)
}
}
})
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for SkipReasonGitignore. The code at lines 467-471 and 637-641 adds this skip reason for gitignore patterns, but there are no tests in the TestSkipHandlerFileCases or TestSkipHandlerDirectoryCases test suites that verify this reason is correctly reported to the skip handler.

Copilot uses AI. Check for mistakes.
Comment thread file_test.go
Comment on lines +938 to +1122
func TestSkipHandlerFileCases(t *testing.T) {
type skipRecord struct {
path string
name string
isDir bool
reason SkipReason
}

type testcase struct {
Name string
Setup func() (*FileWalker, chan *File)
ExpectedSkips int
ExpectedReason SkipReason
ExpectedIsDir bool
}

testCases := []testcase{
{
Name: "ExcludeFilename skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "excluded.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.ExcludeFilename = []string{"excluded.txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonExcludeFilename,
ExpectedIsDir: false,
},
{
Name: "IncludeFilename skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "other.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.IncludeFilename = []string{"wanted.txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonIncludeFilename,
ExpectedIsDir: false,
},
{
Name: "ExcludeFilenameRegex skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.log"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.ExcludeFilenameRegex = []*regexp.Regexp{regexp.MustCompile(`\.log$`)}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonExcludeFilenameRegex,
ExpectedIsDir: false,
},
{
Name: "IncludeFilenameRegex skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.IncludeFilenameRegex = []*regexp.Regexp{regexp.MustCompile(`\.go$`)}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonIncludeFilenameRegex,
ExpectedIsDir: false,
},
{
Name: "AllowListExtension skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.md"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.AllowListExtensions = []string{"go"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonAllowListExtension,
ExpectedIsDir: false,
},
{
Name: "ExcludeListExtension skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.ExcludeListExtensions = []string{"txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonExcludeListExtension,
ExpectedIsDir: false,
},
{
Name: "LocationExcludePattern file skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.txt"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.LocationExcludePattern = []string{"test.txt"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonLocationExcludePattern,
ExpectedIsDir: false,
},
{
Name: "Binary file skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_ = os.WriteFile(filepath.Join(d, "binary.bin"), []byte{0}, 0644)

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.IgnoreBinaryFiles = true
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonBinary,
ExpectedIsDir: false,
},
{
Name: "CustomIgnorePatterns file skip",
Setup: func() (*FileWalker, chan *File) {
d, _ := os.MkdirTemp(os.TempDir(), randSeq(10))
_, _ = os.Create(filepath.Join(d, "test.md"))

fileListQueue := make(chan *File, 10)
walker := NewFileWalker(d, fileListQueue)
walker.CustomIgnorePatterns = []string{"*.md"}
return walker, fileListQueue
},
ExpectedSkips: 1,
ExpectedReason: SkipReasonCustomIgnore,
ExpectedIsDir: false,
},
}

for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
walker, fileListQueue := tc.Setup()

var skips []skipRecord
walker.SetSkipHandler(func(path string, name string, isDir bool, reason SkipReason) {
skips = append(skips, skipRecord{path: path, name: name, isDir: isDir, reason: reason})
})
walker.osReadFile = func(name string) ([]byte, error) { return nil, nil }
_ = walker.Start()

// drain channel
for range fileListQueue {
}

if len(skips) != tc.ExpectedSkips {
t.Errorf("expected %d skips but got %d", tc.ExpectedSkips, len(skips))
return
}

if len(skips) > 0 {
if skips[0].reason != tc.ExpectedReason {
t.Errorf("expected reason %q but got %q", tc.ExpectedReason, skips[0].reason)
}
if skips[0].isDir != tc.ExpectedIsDir {
t.Errorf("expected isDir=%v but got isDir=%v", tc.ExpectedIsDir, skips[0].isDir)
}
}
})
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for SkipReasonIgnoreFile. The code at lines 477-481 and 646-650 adds this skip reason for ignore file patterns, but there are no tests in the TestSkipHandlerFileCases or TestSkipHandlerDirectoryCases test suites that verify this reason is correctly reported to the skip handler.

Copilot uses AI. Check for mistakes.
Comment thread cmd/gocodewalker/main.go
func main() {
fileListQueue := make(chan *gocodewalker.File, 10_000)
fileWalker := gocodewalker.NewParallelFileWalker([]string{"./cmd/", "./go-gitignore/"}, fileListQueue)
fileWalker := gocodewalker.NewParallelFileWalker([]string{"."}, fileListQueue)

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path for NewParallelFileWalker was changed from scanning specific directories (./cmd/, ./go-gitignore/) to scanning the entire current directory (.). This change may have been made for testing the skip handler but appears unrelated to the skip handler feature itself. If this change was unintentional or only for testing purposes, it should be reverted to preserve the original functionality of this example program.

Suggested change
fileWalker := gocodewalker.NewParallelFileWalker([]string{"."}, fileListQueue)
fileWalker := gocodewalker.NewParallelFileWalker([]string{"./cmd/", "./go-gitignore/"}, fileListQueue)

Copilot uses AI. Check for mistakes.
Comment thread file.go
Comment on lines 487 to 492
if ignore.MatchIsDir(joined, false) != nil {
shouldIgnore = ignore.Ignore(joined)
if shouldIgnore {
skipReason = SkipReasonCustomIgnore
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skipReason could be incorrectly set when ignore.Ignore returns false. When shouldIgnore is set to false by ignore.Ignore at line 488, the skipReason retains its previous value. If a subsequent filter check then sets shouldIgnore to true, the skipReason might incorrectly reference an earlier custom ignore rule instead of the actual reason for skipping. Consider only setting skipReason when shouldIgnore transitions from false to true, or clear skipReason when shouldIgnore becomes false.

Copilot uses AI. Check for mistakes.
Comment thread file.go
Comment on lines 637 to 642
if ignore.MatchIsDir(joined, true) != nil {
shouldIgnore = ignore.Ignore(joined)
if shouldIgnore {
skipReason = SkipReasonGitignore
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skipReason could be incorrectly set when ignore.Ignore returns false. When shouldIgnore is set to false by ignore.Ignore at line 638, the skipReason retains its previous value. If a subsequent filter check then sets shouldIgnore to true, the skipReason might incorrectly reference an earlier gitignore rule instead of the actual reason for skipping. Consider only setting skipReason when shouldIgnore transitions from false to true, or clear skipReason when shouldIgnore becomes false.

Copilot uses AI. Check for mistakes.
Comment thread file.go
Comment on lines 675 to 682
if len(f.IncludeDirectory) != 0 {
shouldIgnore = !slices.ContainsFunc(f.IncludeDirectory, func(allow string) bool {
return dir.Name() == allow
})
if shouldIgnore {
skipReason = SkipReasonIncludeDirectory
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IncludeDirectory filter unconditionally overwrites shouldIgnore, which can incorrectly un-skip directories that were already marked for skipping by earlier filters (gitignore, ignore files, custom ignores, or module ignores). If a directory was marked shouldIgnore=true by a previous filter, this check will set shouldIgnore=false if the directory matches IncludeDirectory, effectively undoing the earlier skip decision. This behavior may be intentional, but the skipReason won't be cleared, leading to incorrect reason reporting.

Copilot uses AI. Check for mistakes.
Comment thread file.go
Comment on lines 477 to 482
if ignore.MatchIsDir(joined, false) != nil {
shouldIgnore = ignore.Ignore(joined)
if shouldIgnore {
skipReason = SkipReasonIgnoreFile
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skipReason could be incorrectly set when ignore.Ignore returns false. When shouldIgnore is set to false by ignore.Ignore at line 478, the skipReason retains its previous value. If a subsequent filter check then sets shouldIgnore to true, the skipReason might incorrectly reference an earlier ignore file rule instead of the actual reason for skipping. Consider only setting skipReason when shouldIgnore transitions from false to true, or clear skipReason when shouldIgnore becomes false.

Copilot uses AI. Check for mistakes.
Comment thread README.md

### Skip Handler

You can supply your own skip handler when walking. This allows you to perform an action a file is skipped.

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammatical error: missing "when" between "action" and "a file". Should read: "This allows you to perform an action when a file is skipped."

Suggested change
You can supply your own skip handler when walking. This allows you to perform an action a file is skipped.
You can supply your own skip handler when walking. This allows you to perform an action when a file is skipped.

Copilot uses AI. Check for mistakes.
Comment thread file.go
Comment on lines 694 to 701
if len(f.IncludeDirectoryRegex) != 0 {
shouldIgnore = !slices.ContainsFunc(f.IncludeDirectoryRegex, func(allow *regexp.Regexp) bool {
return allow.MatchString(dir.Name())
})
if shouldIgnore {
skipReason = SkipReasonIncludeDirectoryRegex
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IncludeDirectoryRegex filter unconditionally overwrites shouldIgnore, which can incorrectly un-skip directories that were already marked for skipping by earlier filters. If a directory was marked shouldIgnore=true by a previous filter, this check will set shouldIgnore=false if the directory matches IncludeDirectoryRegex, effectively undoing the earlier skip decision. This behavior may be intentional, but the skipReason won't be cleared, leading to incorrect reason reporting.

Copilot uses AI. Check for mistakes.
Comment thread file.go
Comment on lines 467 to 472
if ignore.MatchIsDir(joined, false) != nil {
shouldIgnore = ignore.Ignore(joined)
if shouldIgnore {
skipReason = SkipReasonGitignore
}
}

Copilot AI Feb 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skipReason could be incorrectly set when ignore.Ignore returns false. When shouldIgnore is set to false by ignore.Ignore at line 468, the skipReason retains its previous value (or remains uninitialized). If a subsequent filter check then sets shouldIgnore to true, the skipReason might incorrectly reference an earlier gitignore rule instead of the actual reason for skipping. Consider only setting skipReason when shouldIgnore transitions from false to true, or clear skipReason when shouldIgnore becomes false.

Copilot uses AI. Check for mistakes.
@boyter
boyter merged commit 060d664 into master Feb 26, 2026
2 checks passed
@boyter
boyter deleted the skipprocessing branch February 26, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

VH/complexity Very high complexity XL/size Extra large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants