Skip to content

.csharpierignore in subdirectories overrides root ignore rules, breaking monorepo scenarios #1783

@BartoGabriel

Description

@BartoGabriel

Problem Summary

When a .csharpierignore file exists in a subdirectory, it completely overrides the patterns defined in the root .csharpierignore for all files within that subdirectory.

This behavior breaks common monorepo and multi-project workflows where you want:

  1. To exclude a subproject directory when formatting from the repository root.
  2. To include files when formatting from within that subproject directory.

Current Behavior

The current implementation in IgnoreFile.cs
(lines 112–118 @ commit 019c82e) searches for .csharpierignore files ascending from the base directory and uses only the first one found:

if (foundCSharpierIgnoreFilePath is null)
{
    var csharpierIgnoreFilePath = fileSystem.Path.Combine(
        directoryInfo.FullName,
        ".csharpierignore"
    );
    if (fileSystem.File.Exists(csharpierIgnoreFilePath))
    {
        foundCSharpierIgnoreFilePath = csharpierIgnoreFilePath;
    }
}

This means that once a .csharpierignore is found in a subdirectory, all parent ignore rules are ignored for files under that directory.


Example Scenario

Project structure

/project-root/
├── .csharpierignore          # Root ignore file
├── src/
│   ├── MainProject/
│   └── Mobile/               # Subproject with different tooling
│       ├── .csharpierignore  # Subproject-specific ignore file
│       └── *.cs files

Root .csharpierignore

src/Mobile/**

src/Mobile/.csharpierignore

**/*.Designer.cs

Expected Behavior

  • dotnet csharpier format . from project root

    • 🚫 src/Mobile/** should be completely ignored
  • dotnet csharpier format . from src/Mobile/

    • ✅ Mobile files should be formatted
    • 🚫 *.Designer.cs files should be ignored

Actual Behavior

  • Running from root:

    • ❌ Mobile files are formatted
      (because src/Mobile/.csharpierignore takes precedence and does not ignore the directory)
  • Running from src/Mobile/:

    • ✅ Behavior is correct

Root Cause

In IgnoreFile.cs
(lines 102–148 @ commit 019c82e), the FindIgnorePaths logic is:

  1. Find the first (closest) .csharpierignore ascending from the directory
  2. Find all .gitignore files ascending until .git/

This results in:

  • Only one .csharpierignore being evaluated per file
  • Parent .csharpierignore files being completely ignored when a child file exists

Proposed Solution (Preferred)

Invert the priority order to match how most ignore systems behave
(ESLint, Prettier, EditorConfig, .gitignore):

  1. Start with root .csharpierignore patterns
  2. Layer more specific patterns from subdirectory .csharpierignore files
  3. Allow negation (!) in child files to override parent rules

Example:

# Root .csharpierignore
src/Mobile/**
# src/Mobile/.csharpierignore
!**/*
**/*.Designer.cs

This would allow directory-level re-enabling while keeping exclusions explicit.


Alternative Solution (Less Breaking)

If inverting priority is too disruptive:

  1. Always evaluate all .csharpierignore files in the ancestor path
  2. Apply patterns from root to leaf
  3. Allow negation patterns to override parent patterns

This would align .csharpierignore behavior with how .gitignore is already handled internally.


Current Workaround

The only available workaround today is to:

  • ❌ Avoid using subdirectory .csharpierignore files entirely
  • ❌ Put formatting rules into .gitignore instead

This is suboptimal because it mixes version control concerns with formatting concerns.


Related Code References

  • Only first .csharpierignore is discovered

    );
    if (fileSystem.File.Exists(csharpierIgnoreFilePath))
    {
    foundCSharpierIgnoreFilePath = csharpierIgnoreFilePath;
    }
    }

  • FindIgnorePaths implementation

    string? foundCSharpierIgnoreFilePath = null;
    var directoryInfo = fileSystem.DirectoryInfo.New(baseDirectoryPath);
    var includeGitIgnores = true;
    while (directoryInfo != null)
    {
    if (foundCSharpierIgnoreFilePath is null)
    {
    var csharpierIgnoreFilePath = fileSystem.Path.Combine(
    directoryInfo.FullName,
    ".csharpierignore"
    );
    if (fileSystem.File.Exists(csharpierIgnoreFilePath))
    {
    foundCSharpierIgnoreFilePath = csharpierIgnoreFilePath;
    }
    }
    if (includeGitIgnores)
    {
    var gitIgnoreFilePath = fileSystem.Path.Combine(
    directoryInfo.FullName,
    ".gitignore"
    );
    if (fileSystem.File.Exists(gitIgnoreFilePath))
    {
    result.Add(gitIgnoreFilePath);
    }
    }
    if (fileSystem.Directory.Exists(Path.Combine(directoryInfo.FullName, ".git")))
    {
    includeGitIgnores = false;
    }
    directoryInfo = directoryInfo.Parent;
    }
    if (foundCSharpierIgnoreFilePath is not null)
    {
    result.Insert(0, foundCSharpierIgnoreFilePath);
    }
    return result;
    }
    }
    internal class InvalidIgnoreFileException(string message, Exception exception)

  • Ignore evaluation logic

    {
    filePath = filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    foreach (var ignore in this.Ignores)
    {
    // when using one of the ignore files to determine if a given file is ignored or not
    // we can only consider that file if it actually has a matching rule for the filePath
    var (hasMatchingRule, isIgnored) = ignore.IsIgnored(filePath);
    if (hasMatchingRule)
    {
    return isIgnored;
    }
    }


Impact

This affects:

  • Monorepos with multiple projects
  • Solutions with subprojects using different tooling
  • Any setup where ignore behavior should depend on invocation context

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions