Skip to content

Commit 551d3b5

Browse files
authored
Modifying CSharpier to support reading gitignore files to determine what files are ignored. (#1484)
Closes #631 CSharpier now works as follows. - .gitignore files are considered when determining if a file will be ignored. A .gitignore file at the same level as a given file will take priority over a .gitignore file above it in the directory tree. - If a .csharpierignore file is present at the same level or anywhere above the given file in the tree and it contains a pattern for a given file, that will take priority. - The patterns within .csharpierignore work the same as a .gitignore, with patterns lower in the file taking priority over patterns above - CSharpier does not currently look further up the directory tree for additional .csharpierignore files if it finds one. But it does look for .gitignore files. If there is demand this could be added later.
1 parent df3da23 commit 551d3b5

File tree

8 files changed

+397
-177
lines changed

8 files changed

+397
-177
lines changed

Src/CSharpier.Cli.Tests/CliTests.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ namespace CSharpier.Cli.Tests;
1515
// are written properly
1616
public class CliTests
1717
{
18-
private static readonly string testFileDirectory = Path.Combine(
19-
Directory.GetCurrentDirectory(),
20-
"TestFiles"
21-
);
18+
private static readonly string testFileDirectory = Directory
19+
.CreateTempSubdirectory("CsharpierTestFies")
20+
.FullName;
2221

2322
[SetUp]
2423
public void BeforeEachTest()
@@ -28,6 +27,12 @@ public void BeforeEachTest()
2827
File.Delete(FormattingCacheFactory.CacheFilePath);
2928
}
3029

30+
Directory.CreateDirectory(testFileDirectory);
31+
}
32+
33+
[TearDown]
34+
public void AfterEachTest()
35+
{
3136
void DeleteDirectory()
3237
{
3338
if (Directory.Exists(testFileDirectory))
@@ -45,8 +50,6 @@ void DeleteDirectory()
4550
Thread.Sleep(TimeSpan.FromMilliseconds(100));
4651
DeleteDirectory();
4752
}
48-
49-
Directory.CreateDirectory(testFileDirectory);
5053
}
5154

5255
[TestCase("\n")]
@@ -173,7 +176,10 @@ public async Task Check_Should_Support_Config_Path()
173176
.ExecuteAsync();
174177

175178
result.ExitCode.Should().Be(1);
176-
result.ErrorOutput.Should().StartWith("Error ./TooWide.cs - Was not formatted.");
179+
result
180+
.ErrorOutput.Replace('\\', '/')
181+
.Should()
182+
.StartWith("Error ./TooWide.cs - Was not formatted.");
177183
}
178184

179185
[Test]
@@ -348,7 +354,7 @@ public async Task Check_Should_Write_Unformatted_File()
348354
.ExecuteAsync();
349355

350356
result
351-
.ErrorOutput.Replace("\\", "/")
357+
.ErrorOutput.Replace('\\', '/')
352358
.Should()
353359
.StartWith("Error ./CheckUnformatted.cs - Was not formatted.");
354360
result.ExitCode.Should().Be(1);

Src/CSharpier.Cli/CommandLineFormatter.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,18 @@ CancellationToken cancellationToken
5454
(
5555
commandLineOptions.IncludeGenerated
5656
|| !GeneratedCodeUtilities.IsGeneratedCodeFile(filePath)
57-
) && !optionsProvider.IsIgnored(filePath)
57+
) && !await optionsProvider.IsIgnoredAsync(filePath, cancellationToken)
5858
)
5959
{
6060
var fileIssueLogger = new FileIssueLogger(
6161
commandLineOptions.OriginalDirectoryOrFilePaths[0],
6262
logger
6363
);
6464

65-
var printerOptions = optionsProvider.GetPrinterOptionsFor(filePath);
65+
var printerOptions = await optionsProvider.GetPrinterOptionsForAsync(
66+
filePath,
67+
cancellationToken
68+
);
6669
if (printerOptions is { Formatter: not Formatter.Unknown })
6770
{
6871
printerOptions.IncludeGenerated = commandLineOptions.IncludeGenerated;
@@ -148,7 +151,7 @@ CancellationToken cancellationToken
148151

149152
for (var x = 0; x < commandLineOptions.DirectoryOrFilePaths.Length; x++)
150153
{
151-
var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x].Replace("\\", "/");
154+
var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x];
152155
var isFile = fileSystem.File.Exists(directoryOrFilePath);
153156
var isDirectory = fileSystem.Directory.Exists(directoryOrFilePath);
154157

@@ -175,9 +178,7 @@ CancellationToken cancellationToken
175178
cancellationToken
176179
);
177180

178-
var originalDirectoryOrFile = commandLineOptions
179-
.OriginalDirectoryOrFilePaths[x]
180-
.Replace("\\", "/");
181+
var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[x];
181182

182183
var formattingCache = await FormattingCacheFactory.InitializeAsync(
183184
commandLineOptions,
@@ -190,7 +191,8 @@ CancellationToken cancellationToken
190191
{
191192
if (!originalDirectoryOrFile.StartsWith('.'))
192193
{
193-
originalDirectoryOrFile = "./" + originalDirectoryOrFile;
194+
originalDirectoryOrFile =
195+
"." + Path.DirectorySeparatorChar + originalDirectoryOrFile;
194196
}
195197
}
196198

@@ -204,13 +206,16 @@ async Task FormatFile(
204206
(
205207
!commandLineOptions.IncludeGenerated
206208
&& GeneratedCodeUtilities.IsGeneratedCodeFile(actualFilePath)
207-
) || optionsProvider.IsIgnored(actualFilePath)
209+
) || await optionsProvider.IsIgnoredAsync(actualFilePath, cancellationToken)
208210
)
209211
{
210212
return;
211213
}
212214

213-
var printerOptions = optionsProvider.GetPrinterOptionsFor(actualFilePath);
215+
var printerOptions = await optionsProvider.GetPrinterOptionsForAsync(
216+
actualFilePath,
217+
cancellationToken
218+
);
214219

215220
if (printerOptions is { Formatter: not Formatter.Unknown })
216221
{
@@ -260,13 +265,8 @@ await FormatPhysicalFile(
260265
SearchOption.AllDirectories
261266
)
262267
.Select(o =>
263-
{
264-
var normalizedPath = o.Replace("\\", "/");
265-
return FormatFile(
266-
normalizedPath,
267-
normalizedPath.Replace(directoryOrFilePath, originalDirectoryOrFile)
268-
);
269-
})
268+
FormatFile(o, o.Replace(directoryOrFilePath, originalDirectoryOrFile))
269+
)
270270
.ToArray();
271271
try
272272
{

Src/CSharpier.Cli/IgnoreFile.cs

Lines changed: 130 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,179 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.IO.Abstractions;
3+
using Ignore;
24

35
namespace CSharpier.Cli;
46

57
internal class IgnoreFile
68
{
7-
protected Ignore.Ignore Ignore { get; }
8-
protected string IgnoreBaseDirectoryPath { get; }
9+
private List<IgnoreWithBasePath> ignores { get; }
910
private static readonly string[] alwaysIgnored = ["**/node_modules", "**/obj", "**/.git"];
1011

11-
protected IgnoreFile(Ignore.Ignore ignore, string ignoreBaseDirectoryPath)
12+
private IgnoreFile(List<IgnoreWithBasePath> ignores)
1213
{
13-
this.Ignore = ignore;
14-
this.IgnoreBaseDirectoryPath = ignoreBaseDirectoryPath.Replace('\\', '/');
14+
this.ignores = ignores;
1515
}
1616

1717
public bool IsIgnored(string filePath)
1818
{
19-
var pathRelativeToIgnoreFile = filePath.Replace('\\', '/');
20-
if (
21-
!pathRelativeToIgnoreFile.StartsWith(
22-
this.IgnoreBaseDirectoryPath,
23-
StringComparison.Ordinal
24-
)
25-
)
19+
filePath = filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
20+
foreach (var ignore in this.ignores)
2621
{
27-
// TODO #631
28-
// the ignore file was created for /test/subfolder
29-
// and we are checking if /test/.editorconfig is ignored, which it CAN'T be
30-
// in main, we did not check if parent folder editorconfigs were ignored, which is probably a bug
31-
// we need to figure out a better way to deal with ignorefiles, which will come in the PR for 631
32-
33-
return false;
34-
// throw new Exception(
35-
// $"The filePath of {filePath} does not start with the ignoreBaseDirectoryPath of {this.IgnoreBaseDirectoryPath}"
36-
// );
22+
// when using one of the ignore files to determine if a given file is ignored or not
23+
// we can only consider that file if it actually has a matching rule for the filePath
24+
var (hasMatchingRule, isIgnored) = ignore.IsIgnored(filePath);
25+
if (hasMatchingRule)
26+
{
27+
return isIgnored;
28+
}
3729
}
3830

39-
pathRelativeToIgnoreFile =
40-
pathRelativeToIgnoreFile.Length > this.IgnoreBaseDirectoryPath.Length
41-
? pathRelativeToIgnoreFile[(this.IgnoreBaseDirectoryPath.Length + 1)..]
42-
: string.Empty;
43-
44-
return this.Ignore.IsIgnored(pathRelativeToIgnoreFile);
31+
return false;
4532
}
4633

47-
public static async Task<IgnoreFile> Create(
34+
public static async Task<IgnoreFile?> CreateAsync(
4835
string baseDirectoryPath,
4936
IFileSystem fileSystem,
5037
CancellationToken cancellationToken
5138
)
5239
{
53-
var ignore = new Ignore.Ignore();
54-
55-
foreach (var name in alwaysIgnored)
40+
var ignoreFilePaths = FindIgnorePaths(baseDirectoryPath, fileSystem);
41+
if (ignoreFilePaths.Count == 0)
5642
{
57-
ignore.Add(name);
43+
var ignore = new IgnoreWithBasePath(baseDirectoryPath);
44+
foreach (var name in alwaysIgnored)
45+
{
46+
ignore.Add(name);
47+
}
48+
return new IgnoreFile([ignore]);
5849
}
5950

60-
var ignoreFilePath = FindIgnorePath(baseDirectoryPath, fileSystem);
61-
if (ignoreFilePath == null)
51+
var ignores = new List<IgnoreWithBasePath>();
52+
foreach (var ignoreFilePath in ignoreFilePaths)
6253
{
63-
return new IgnoreFile(ignore, baseDirectoryPath);
54+
var ignore = new IgnoreWithBasePath(Path.GetDirectoryName(ignoreFilePath)!);
55+
ignores.Add(ignore);
56+
foreach (var name in alwaysIgnored)
57+
{
58+
ignore.Add(name);
59+
}
60+
foreach (
61+
var line in await fileSystem.File.ReadAllLinesAsync(
62+
ignoreFilePath,
63+
cancellationToken
64+
)
65+
)
66+
{
67+
if (string.IsNullOrWhiteSpace(line))
68+
{
69+
continue;
70+
}
71+
try
72+
{
73+
ignore.Add(line);
74+
}
75+
catch (Exception ex)
76+
{
77+
throw new InvalidIgnoreFileException(
78+
$"""
79+
The .csharpierignore file at {ignoreFilePath} could not be parsed due to the following line:
80+
{line}
81+
""",
82+
ex
83+
);
84+
}
85+
}
6486
}
6587

66-
foreach (
67-
var line in await fileSystem.File.ReadAllLinesAsync(ignoreFilePath, cancellationToken)
68-
)
88+
return new IgnoreFile(ignores);
89+
}
90+
91+
// this will return the ignore paths in order of priority
92+
// the first csharpierignore it finds at or above the path
93+
// and then all .gitignores (at or above) it finds in order from closest to further away
94+
private static List<string> FindIgnorePaths(string baseDirectoryPath, IFileSystem fileSystem)
95+
{
96+
var result = new List<string>();
97+
string? foundCSharpierIgnoreFilePath = null;
98+
var directoryInfo = fileSystem.DirectoryInfo.New(baseDirectoryPath);
99+
while (directoryInfo != null)
69100
{
70-
try
101+
if (foundCSharpierIgnoreFilePath is null)
71102
{
72-
ignore.Add(line);
103+
var csharpierIgnoreFilePath = fileSystem.Path.Combine(
104+
directoryInfo.FullName,
105+
".csharpierignore"
106+
);
107+
if (fileSystem.File.Exists(csharpierIgnoreFilePath))
108+
{
109+
foundCSharpierIgnoreFilePath = csharpierIgnoreFilePath;
110+
}
73111
}
74-
catch (Exception ex)
112+
113+
var gitIgnoreFilePath = fileSystem.Path.Combine(directoryInfo.FullName, ".gitignore");
114+
if (fileSystem.File.Exists(gitIgnoreFilePath))
75115
{
76-
throw new InvalidIgnoreFileException(
77-
@$"The .csharpierignore file at {ignoreFilePath} could not be parsed due to the following line:
78-
{line}
79-
",
80-
ex
81-
);
116+
result.Add(gitIgnoreFilePath);
82117
}
83-
}
84118

85-
var directoryName = fileSystem.Path.GetDirectoryName(ignoreFilePath);
119+
directoryInfo = directoryInfo.Parent;
120+
}
86121

87-
ArgumentNullException.ThrowIfNull(directoryName);
122+
if (foundCSharpierIgnoreFilePath is not null)
123+
{
124+
result.Insert(0, foundCSharpierIgnoreFilePath);
125+
}
88126

89-
return new IgnoreFile(ignore, directoryName);
127+
return result;
90128
}
91129

92-
private static string? FindIgnorePath(string baseDirectoryPath, IFileSystem fileSystem)
130+
// modified from the nuget library to include the directory
131+
// that the ignore file exists at
132+
// and to return if this ignore file has a rule for a given path
133+
private class IgnoreWithBasePath(string basePath)
93134
{
94-
var directoryInfo = fileSystem.DirectoryInfo.New(baseDirectoryPath);
95-
while (directoryInfo != null)
135+
private readonly List<IgnoreRule> Rules = new();
136+
137+
public (bool hasMatchingRule, bool isIgnored) IsIgnored(string path)
96138
{
97-
var ignoreFilePath = fileSystem.Path.Combine(
98-
directoryInfo.FullName,
99-
".csharpierignore"
100-
);
101-
if (fileSystem.File.Exists(ignoreFilePath))
139+
if (!path.StartsWith(basePath, StringComparison.Ordinal))
102140
{
103-
return ignoreFilePath;
141+
return (false, false);
104142
}
105143

106-
directoryInfo = directoryInfo.Parent;
144+
var pathRelativeToIgnoreFile =
145+
path.Length > basePath.Length
146+
? path[(basePath.Length + 1)..].Replace('\\', '/')
147+
: string.Empty;
148+
149+
var isIgnored = false;
150+
var hasMatchingRule = false;
151+
foreach (var rule in this.Rules)
152+
{
153+
var isMatch = rule.IsMatch(pathRelativeToIgnoreFile);
154+
if (isMatch)
155+
{
156+
hasMatchingRule = true;
157+
}
158+
if (rule.Negate)
159+
{
160+
if (isIgnored && isMatch)
161+
{
162+
isIgnored = false;
163+
}
164+
}
165+
else if (!isIgnored && isMatch)
166+
{
167+
isIgnored = true;
168+
}
169+
}
170+
return (hasMatchingRule, isIgnored);
107171
}
108172

109-
return null;
173+
public void Add(string rule)
174+
{
175+
this.Rules.Add(new IgnoreRule(rule));
176+
}
110177
}
111178
}
112179

0 commit comments

Comments
 (0)