Skip to content

Commit

Permalink
WildcardPatternMatcher improvement (#6919)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristian-ambrosini-sonarsource committed Mar 16, 2023
1 parent 62efe30 commit a60ea62
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 128 deletions.
Expand Up @@ -142,8 +142,8 @@ public bool HasMatchingScope(DiagnosticDescriptor descriptor)
&& !IsExcluded(globalExclusions, filePath);

private static bool IsIncluded(string[] inclusions, string filePath) =>
inclusions is { Length: 0 } || inclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath));
inclusions is { Length: 0 } || inclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath, true));

private static bool IsExcluded(string[] exclusions, string filePath) =>
exclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath));
exclusions.Any(x => WildcardPatternMatcher.IsMatch(x, filePath, false));
}
134 changes: 51 additions & 83 deletions analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs
Expand Up @@ -27,108 +27,76 @@ namespace SonarAnalyzer.Helpers;

internal static class WildcardPatternMatcher
{
public static bool IsMatch(string pattern, string input) =>
private static readonly ConcurrentDictionary<string, Regex> Cache = new();

public static bool IsMatch(string pattern, string input, bool timeoutFallbackResult) =>
!(string.IsNullOrWhiteSpace(pattern) || string.IsNullOrWhiteSpace(input))
&& WildcardPattern.Create(pattern).Match(input);
&& Cache.GetOrAdd(pattern, _ => new Regex(ToRegex(pattern), RegexOptions.None, RegexConstants.DefaultTimeout)) is var regex
&& IsMatch(regex, input, timeoutFallbackResult);

/// <summary>
/// Copied from https://github.com/SonarSource/sonar-plugin-api/blob/a9bd7ff48f0f77811ed909070030678c443c975a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WildcardPattern.java.
/// </summary>
private sealed class WildcardPattern
private static bool IsMatch(Regex regex, string value, bool timeoutFallbackResult)
{
private const string SpecialChars = "()[]^$.{}+|";
private static readonly ConcurrentDictionary<string, WildcardPattern> Cache = new();
private readonly Regex pattern;

private WildcardPattern(string pattern, string directorySeparator) =>
this.pattern = new Regex(ToRegexp(pattern, directorySeparator), RegexOptions.Compiled, RegexConstants.DefaultTimeout);

public bool Match(string value)
try
{
value = value.TrimStart('/');
value = value.TrimEnd('/');
try
{
return pattern.IsMatch(value);
}
catch (RegexMatchTimeoutException)
{
return false;
}
return regex.IsMatch(value.Trim('/'));
}

public static WildcardPattern Create(string pattern) =>
Create(pattern, Path.DirectorySeparatorChar.ToString());

private static WildcardPattern Create(string pattern, string directorySeparator) =>
Cache.GetOrAdd(pattern + directorySeparator, _ => new WildcardPattern(pattern, directorySeparator));

private static string ToRegexp(string wildcardPattern, string directorySeparator)
catch (RegexMatchTimeoutException)
{
var escapedDirectorySeparator = '\\' + directorySeparator;
var sb = new StringBuilder(wildcardPattern.Length);

sb.Append('^');
return timeoutFallbackResult;
}
}

var i = wildcardPattern.StartsWith("/") || wildcardPattern.StartsWith("\\") ? 1 : 0;
while (i < wildcardPattern.Length)
/// <summary>
/// Copied from https://github.com/SonarSource/sonar-plugin-api/blob/a9bd7ff48f0f77811ed909070030678c443c975a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WildcardPattern.java.
/// </summary>
private static string ToRegex(string wildcardPattern)
{
var escapedDirectorySeparator = Regex.Escape(Path.DirectorySeparatorChar.ToString());
var sb = new StringBuilder("^", wildcardPattern.Length);
var i = IsSlash(wildcardPattern[0]) ? 1 : 0;
while (i < wildcardPattern.Length)
{
var ch = wildcardPattern[i];
if (ch == '*')
{
var ch = wildcardPattern[i];

if (SpecialChars.IndexOf(ch) != -1)
if (i + 1 < wildcardPattern.Length && wildcardPattern[i + 1] == '*')
{
// Escape regexp-specific characters
sb.Append('\\').Append(ch);
}
else if (ch == '*')
{
if (i + 1 < wildcardPattern.Length && wildcardPattern[i + 1] == '*')
// Double asterisk - Zero or more directories
if (i + 2 < wildcardPattern.Length && IsSlash(wildcardPattern[i + 2]))
{
// Double asterisk
// Zero or more directories
if (i + 2 < wildcardPattern.Length && IsSlash(wildcardPattern[i + 2]))
{
sb.Append("(?:.*").Append(escapedDirectorySeparator).Append("|)");
i += 2;
}
else
{
sb.Append(".*");
i += 1;
}
sb.Append($"(.*{escapedDirectorySeparator}|)");
i += 2;
}
else
{
// Single asterisk
// Zero or more characters excluding directory separator
sb.Append("[^").Append(escapedDirectorySeparator).Append("]*?");
sb.Append(".*");
i += 1;
}
}
else if (ch == '?')
{
// Any single character excluding directory separator
sb.Append("[^").Append(escapedDirectorySeparator).Append("]");
}
else if (IsSlash(ch))
{
// Directory separator
sb.Append(escapedDirectorySeparator);
}
else
{
// Single character
sb.Append(ch);
// Single asterisk - Zero or more characters excluding directory separator
sb.Append($"[^{escapedDirectorySeparator}]*?");
}

i++;
}

sb.Append('$');

return sb.ToString();
else if (ch == '?')
{
// Any single character excluding directory separator
sb.Append($"[^{escapedDirectorySeparator}]");
}
else if (IsSlash(ch))
{
sb.Append(escapedDirectorySeparator);
}
else
{
sb.Append(Regex.Escape(ch.ToString()));
}
i++;
}

private static bool IsSlash(char ch) =>
ch == '/' || ch == '\\';
return sb.Append('$').ToString();
}

private static bool IsSlash(char ch) =>
ch == '/' || ch == '\\';
}
Expand Up @@ -29,87 +29,66 @@ public class WildcardPatternMatcherTest
/// Based on https://github.com/SonarSource/sonar-plugin-api/blob/master/plugin-api/src/test/java/org/sonar/api/utils/WildcardPatternTest.java.
/// </summary>
[DataTestMethod]

[DataRow("Foo", "Foo", true)]
[DataRow("foo", "FOO", false)]
[DataRow("Foo", "Foot", false)]
[DataRow("Foo", "Bar", false)]

[DataRow("org/T?st.java", "org/Test.java", true)]
[DataRow("org/T?st.java", "org/Tost.java", true)]
[DataRow("org/T?st.java", "org/Teeest.java", false)]

[DataRow("org/*.java", "org/Foo.java", true)]
[DataRow("org/*.java", "org/Bar.java", true)]

[DataRow("org/**", "org/Foo.java", true)]
[DataRow("org/T?st.cs", "org/Test.cs", true)]
[DataRow("org/T?st.cs", "org/Tost.cs", true)]
[DataRow("org/T?st.cs", "org/Teeest.cs", false)]
[DataRow("org/*.cs", "org/Foo.cs", true)]
[DataRow("org/*.cs", "org/Bar.cs", true)]
[DataRow("org/**", "org/Foo.cs", true)]
[DataRow("org/**", "org/foo/bar.jsp", true)]

[DataRow("org/**/Test.java", "org/Test.java", true)]
[DataRow("org/**/Test.java", "org/foo/Test.java", true)]
[DataRow("org/**/Test.java", "org/foo/bar/Test.java", true)]

[DataRow("org/**/*.java", "org/Foo.java", true)]
[DataRow("org/**/*.java", "org/foo/Bar.java", true)]
[DataRow("org/**/*.java", "org/foo/bar/Baz.java", true)]

[DataRow("o?/**/*.java", "org/test.java", false)]
[DataRow("o?/**/*.java", "o/test.java", false)]
[DataRow("o?/**/*.java", "og/test.java", true)]
[DataRow("o?/**/*.java", "og/foo/bar/test.java", true)]
[DataRow("o?/**/*.java", "og/foo/bar/test.jav", false)]

[DataRow("org/**/Test.cs", "org/Test.cs", true)]
[DataRow("org/**/Test.cs", "org/foo/Test.cs", true)]
[DataRow("org/**/Test.cs", "org/foo/bar/Test.cs", true)]
[DataRow("org/**/*.cs", "org/Foo.cs", true)]
[DataRow("org/**/*.cs", "org/foo/Bar.cs", true)]
[DataRow("org/**/*.cs", "org/foo/bar/Baz.cs", true)]
[DataRow("o?/**/*.cs", "org/test.cs", false)]
[DataRow("o?/**/*.cs", "o/test.cs", false)]
[DataRow("o?/**/*.cs", "og/test.cs", true)]
[DataRow("o?/**/*.cs", "og/foo/bar/test.cs", true)]
[DataRow("o?/**/*.cs", "og/foo/bar/test.c", false)]
[DataRow("org/sonar/**", "org/sonar/commons/Foo", true)]
[DataRow("org/sonar/**", "org/sonar/Foo.java", true)]

[DataRow("org/sonar/**", "org/sonar/Foo.cs", true)]
[DataRow("xxx/org/sonar/**", "org/sonar/Foo", false)]

[DataRow("org/sonar/**/**", "org/sonar/commons/Foo", true)]
[DataRow("org/sonar/**/**", "org/sonar/commons/sub/Foo.java", true)]

[DataRow("org/sonar/**/**", "org/sonar/commons/sub/Foo.cs", true)]
[DataRow("org/sonar/**/Foo", "org/sonar/commons/sub/Foo", true)]
[DataRow("org/sonar/**/Foo", "org/sonar/Foo", true)]

[DataRow("*/foo/*", "org/foo/Bar", true)]
[DataRow("*/foo/*", "foo/Bar", false)]
[DataRow("*/foo/*", "foo", false)]
[DataRow("*/foo/*", "org/foo/bar/Hello", false)]

[DataRow("hell?", "hell", false)]
[DataRow("hell?", "hello", true)]
[DataRow("hell?", "helloworld", false)]

[DataRow("**/Reader", "java/io/Reader", true)]
[DataRow("**/Reader", "org/sonar/channel/CodeReader", false)]

[DataRow("**", "java/io/Reader", true)]

[DataRow("**/app/**", "com/app/Utils", true)]
[DataRow("**/app/**", "com/application/MyService", false)]

[DataRow("**/*$*", "foo/bar", false)]
[DataRow("**/*$*", "foo/bar$baz", true)]
[DataRow("a+", "aa", false)]
[DataRow("a+", "a+", true)]
[DataRow("[ab]", "a", false)]
[DataRow("[ab]", "[ab]", true)]

[DataRow("\\n", "\n", false)]
[DataRow("foo\\bar", "foo/bar", true)]

[DataRow("/foo", "foo", true)]
[DataRow("\\foo", "foo", true)]

[DataRow("foo\\bar", "foo\\bar", true)]
[DataRow("foo/bar", "foo\\bar", true)]
[DataRow("foo\\bar/baz", "foo\\bar\\baz", true)]

public void IsMatch_MatchesPatternsAsExpected(string pattern, string input, bool expectedResult)
{
// The test cases are copied from the plugin-api and the directory separators need replacing as Roslyn will not give us the paths with '/'.
input = input.Replace("/", Path.DirectorySeparatorChar.ToString());

WildcardPatternMatcher.IsMatch(pattern, input).Should().Be(expectedResult);
WildcardPatternMatcher.IsMatch(pattern, input, false).Should().Be(expectedResult);
}

[DataTestMethod]
Expand All @@ -118,12 +97,12 @@ public void IsMatch_MatchesPatternsAsExpected(string pattern, string input, bool
[DataRow("/")]
[DataRow("\\")]
public void IsMatch_InvalidPattern_ReturnsFalse(string pattern) =>
WildcardPatternMatcher.IsMatch(pattern, "foo").Should().BeFalse();
WildcardPatternMatcher.IsMatch(pattern, "foo", false).Should().BeFalse();

[DataTestMethod]
[DataRow(null, "foo")]
[DataRow("foo", null)]
public void IsMatch_InputParametersArenull_DoesNotThrow(string pattern, string input) =>
WildcardPatternMatcher.IsMatch(pattern, input).Should().BeFalse();
WildcardPatternMatcher.IsMatch(pattern, input, false).Should().BeFalse();
}
}

0 comments on commit a60ea62

Please sign in to comment.