Skip to content

Commit

Permalink
Ensure source paths are comparable with editorconfig directory paths (#…
Browse files Browse the repository at this point in the history
…73100)

* Collapse separators with forward slash

* Improve code

* Clarify test parameter names
  • Loading branch information
jjonescz committed May 2, 2024
1 parent d8f3b0b commit fa7ece1
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 1 deletion.
57 changes: 57 additions & 0 deletions src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,63 @@ class C
Assert.Null(cmd.AnalyzerOptions);
}

[Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/72657")]
public void AnalyzerConfig_DoubleSlash(bool doubleSlashAnalyzerConfig, bool doubleSlashSource)
{
var dir = Temp.CreateDirectory();
var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true);
var src = dir.CreateFile("Class1.cs").WriteAllText("""
public class C
{
public void M() { }
}
""");

// The analyzer should produce a warning.
var output = VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, analyzers: [analyzer], expectedWarningCount: 1);
AssertEx.Equal("Class1.cs(1,1): warning ID1000:", output.Trim());

// But not when this editorconfig is applied.
var editorconfig = dir.CreateFile(".editorconfig").WriteAllText("""
root = true

[*.cs]
dotnet_analyzer_diagnostic.severity = none

generated_code = true
""");
var cmd = CreateCSharpCompiler(
[
"/nologo",
"/preferreduilang:en",
"/t:library",
"/analyzerconfig:" + modifyPath(editorconfig.Path, doubleSlashAnalyzerConfig),
modifyPath(src.Path, doubleSlashSource),
],
[analyzer]);
var outWriter = new StringWriter(CultureInfo.InvariantCulture);
var exitCode = cmd.Run(outWriter);
Assert.Equal(0, exitCode);
AssertEx.Equal("", outWriter.ToString());

static string modifyPath(string path, bool doubleSlash)
{
if (!doubleSlash)
{
return path;
}

// Find the second-to-last slash.
char[] separators = ['/', '\\'];
var lastSlashIndex = path.LastIndexOfAny(separators);
lastSlashIndex = path.LastIndexOfAny(separators, lastSlashIndex - 1);

// Duplicate that slash.
var lastSlash = path[lastSlashIndex];
return path[0..lastSlashIndex] + lastSlash + path[lastSlashIndex..];
}
}

[Fact]
public void AnalyzerConfigWithOptions()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,33 @@ public void EditorConfigToDiagnostics()
}, options.Select(o => o.TreeOptions).ToArray());
}

[Theory, WorkItem("https://github.com/dotnet/roslyn/issues/72657")]
[InlineData("/", "/")]
[InlineData("/a/b/c/", "/a/b/c/")]
[InlineData("/a/b//c/", "/a/b/c/")]
[InlineData("/a/b/c/", "/a/b//c/")]
[InlineData("/a/b//c/", "/a/b//c/")]
[InlineData("/a/b/c//", "/a/b/c/")]
[InlineData("/a/b/c/", "/a/b/c//")]
[InlineData("/a/b/c//", "/a/b/c//")]
[InlineData("/a/b//c/", "/a/b///c/")]
public void EditorConfigToDiagnostics_DoubleSlash(string prefixEditorConfig, string prefixSource)
{
var configs = ArrayBuilder<AnalyzerConfig>.GetInstance();
configs.Add(Parse("""
[*.cs]
dotnet_diagnostic.cs000.severity = none
""",
prefixEditorConfig + ".editorconfig"));

var options = GetAnalyzerConfigOptions([prefixSource + "test.cs"], configs);
configs.Free();

Assert.Equal([
CreateImmutableDictionary(("cs000", ReportDiagnostic.Suppress))
], options.Select(o => o.TreeOptions).ToArray());
}

[Fact]
public void LaterSectionOverrides()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,5 +420,20 @@ public void GetRelativePath_EnsureNo_IndexOutOfRangeException_Unix()
var result = PathUtilities.GetRelativePath(@"/A/B/", @"/A/B");
Assert.Equal(expected, result);
}

[Theory]
[InlineData(@"//a/b/c", @"//a/b/c")]
[InlineData(@"/a\b/c/", @"/a/b/c/")]
[InlineData(@"\a\b/c/", @"/a/b/c/")]
[InlineData(@"C:\\a", @"C:/a")]
[InlineData(@"C:\a\b\c\", @"C:/a/b/c/")]
[InlineData(@"/\a", @"//a")]
[InlineData(@"a\\\b", @"a/b")]
[InlineData(@"\\\a\b\c", @"///a/b/c")]
[InlineData(@"\\\\a\b\c", @"///a/b/c")]
public void CollapseWithForwardSlash(string input, string output)
{
AssertEx.Equal(output, PathUtilities.CollapseWithForwardSlash(input.AsSpan()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public AnalyzerConfigOptionsResult GetOptionsForSourcePath(string sourcePath)

var sectionKey = _sectionKeyPool.Allocate();

var normalizedPath = PathUtilities.NormalizeWithForwardSlash(sourcePath);
var normalizedPath = PathUtilities.CollapseWithForwardSlash(sourcePath.AsSpan());
normalizedPath = PathUtilities.ExpandAbsolutePathWithRelativeParts(normalizedPath);

// If we have a global config, add any sections that match the full path. We can have at most one section since
Expand Down
37 changes: 37 additions & 0 deletions src/Compilers/Core/Portable/FileSystem/PathUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.PooledObjects;

Expand Down Expand Up @@ -780,6 +781,42 @@ public static bool IsValidFilePath([NotNullWhen(true)] string? fullPath)
public static string NormalizeWithForwardSlash(string p)
=> DirectorySeparatorChar == '/' ? p : p.Replace(DirectorySeparatorChar, '/');

/// <summary>
/// Replaces all sequences of '\' or '/' with a single '/' but preserves UNC prefix '//'.
/// </summary>
public static string CollapseWithForwardSlash(ReadOnlySpan<char> path)
{
var sb = new StringBuilder(path.Length);

int start = 0;
if (path.Length > 1 && IsAnyDirectorySeparator(path[0]) && IsAnyDirectorySeparator(path[1]))
{
// Preserve UNC paths.
sb.Append("//");
start = 2;
}

bool wasDirectorySeparator = false;
for (int i = start; i < path.Length; i++)
{
if (IsAnyDirectorySeparator(path[i]))
{
if (!wasDirectorySeparator)
{
sb.Append('/');
}
wasDirectorySeparator = true;
}
else
{
sb.Append(path[i]);
wasDirectorySeparator = false;
}
}

return sb.ToString();
}

/// <summary>
/// Takes an absolute path and attempts to expand any '..' or '.' into their equivalent representation.
/// </summary>
Expand Down

0 comments on commit fa7ece1

Please sign in to comment.