diff --git a/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs b/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs index 90889c8ffd0..cc9dedd3618 100644 --- a/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs +++ b/analyzers/src/SonarAnalyzer.Common/AnalysisContext/SonarAnalysisContextBase.cs @@ -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)); } diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs index b72a702d8e9..1054d7d867b 100644 --- a/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs +++ b/analyzers/src/SonarAnalyzer.Common/Helpers/WildcardPatternMatcher.cs @@ -27,108 +27,76 @@ namespace SonarAnalyzer.Helpers; internal static class WildcardPatternMatcher { - public static bool IsMatch(string pattern, string input) => + private static readonly ConcurrentDictionary 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); - /// - /// Copied from https://github.com/SonarSource/sonar-plugin-api/blob/a9bd7ff48f0f77811ed909070030678c443c975a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WildcardPattern.java. - /// - private sealed class WildcardPattern + private static bool IsMatch(Regex regex, string value, bool timeoutFallbackResult) { - private const string SpecialChars = "()[]^$.{}+|"; - private static readonly ConcurrentDictionary 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) + /// + /// Copied from https://github.com/SonarSource/sonar-plugin-api/blob/a9bd7ff48f0f77811ed909070030678c443c975a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WildcardPattern.java. + /// + 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 == '\\'; } diff --git a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs index f8b02ce4aa8..79273d566d2 100644 --- a/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs +++ b/analyzers/tests/SonarAnalyzer.UnitTest/Helpers/WildcardPatternMatcherTest.cs @@ -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. /// [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] @@ -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(); } }