-
Notifications
You must be signed in to change notification settings - Fork 157
Remove slow regex from threading analyzers #1547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "servers": { | ||
| "github": { | ||
| "url": "https://api.githubcopilot.com/mcp/" | ||
| } | ||
| }, | ||
| "inputs": [] | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -69,17 +69,6 @@ public static class CommonInterest | |||||
|
|
||||||
| private const string GetAwaiterMethodName = "GetAwaiter"; | ||||||
|
|
||||||
| private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // Prevent expensive CPU hang in Regex.Match if backtracking occurs due to pathological input (see #485). | ||||||
|
|
||||||
| private static readonly Regex NegatableTypeOrMemberReferenceRegex = new Regex(@"^(?<negated>!)?\[(?<typeName>[^\[\]\:]+)+\](?:\:\:(?<memberName>\S+))?\s*$", RegexOptions.Singleline | RegexOptions.CultureInvariant, RegexMatchTimeout); | ||||||
|
|
||||||
| private static readonly Regex MemberReferenceRegex = new Regex(@"^\[(?<typeName>[^\[\]\:]+)+\]::(?<memberName>\S+)\s*$", RegexOptions.Singleline | RegexOptions.CultureInvariant, RegexMatchTimeout); | ||||||
|
|
||||||
| /// <summary> | ||||||
| /// An array with '.' as its only element. | ||||||
| /// </summary> | ||||||
| private static readonly char[] QualifiedIdentifierSeparators = ['.']; | ||||||
|
|
||||||
| public static IEnumerable<QualifiedMember> ReadMethods(AnalyzerOptions analyzerOptions, Regex fileNamePattern, CancellationToken cancellationToken) | ||||||
| { | ||||||
| foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken)) | ||||||
|
|
@@ -92,28 +81,15 @@ public static IEnumerable<TypeMatchSpec> ReadTypesAndMembers(AnalyzerOptions ana | |||||
| { | ||||||
| foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken)) | ||||||
| { | ||||||
| Match? match = null; | ||||||
| try | ||||||
| { | ||||||
| match = NegatableTypeOrMemberReferenceRegex.Match(line); | ||||||
| } | ||||||
| catch (RegexMatchTimeoutException) | ||||||
| { | ||||||
| throw new InvalidOperationException($"Regex.Match timeout when parsing line: {line}"); | ||||||
| } | ||||||
|
|
||||||
| if (!match.Success) | ||||||
| if (!CommonInterestParsing.TryParseNegatableTypeOrMemberReference(line, out bool negated, out ReadOnlyMemory<char> typeNameMemory, out string? memberNameValue)) | ||||||
| { | ||||||
| throw new InvalidOperationException($"Parsing error on line: {line}"); | ||||||
| } | ||||||
|
|
||||||
| bool inverted = match.Groups["negated"].Success; | ||||||
| string[] typeNameElements = match.Groups["typeName"].Value.Split(QualifiedIdentifierSeparators); | ||||||
| string typeName = typeNameElements[typeNameElements.Length - 1]; | ||||||
| var containingNamespace = typeNameElements.Take(typeNameElements.Length - 1).ToImmutableArray(); | ||||||
| (ImmutableArray<string> containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory); | ||||||
| var type = new QualifiedType(containingNamespace, typeName); | ||||||
| QualifiedMember member = match.Groups["memberName"].Success ? new QualifiedMember(type, match.Groups["memberName"].Value) : default(QualifiedMember); | ||||||
| yield return new TypeMatchSpec(type, member, inverted); | ||||||
| QualifiedMember member = memberNameValue is not null ? new QualifiedMember(type, memberNameValue) : default(QualifiedMember); | ||||||
| yield return new TypeMatchSpec(type, member, negated); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -345,26 +321,47 @@ public static IEnumerable<string> ReadLinesFromAdditionalFile(SourceText text) | |||||
|
|
||||||
| public static QualifiedMember ParseAdditionalFileMethodLine(string line) | ||||||
| { | ||||||
| Match? match = null; | ||||||
| try | ||||||
| if (!CommonInterestParsing.TryParseMemberReference(line, out ReadOnlyMemory<char> typeNameMemory, out string? memberName)) | ||||||
| { | ||||||
| match = MemberReferenceRegex.Match(line); | ||||||
| throw new InvalidOperationException($"Parsing error on line: {line}"); | ||||||
| } | ||||||
| catch (RegexMatchTimeoutException) | ||||||
|
|
||||||
| (ImmutableArray<string> containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory); | ||||||
|
||||||
| (ImmutableArray<string> containingNamespace, string? typeName) = SplitQualifiedIdentifier(typeNameMemory); | |
| (ImmutableArray<string> containingNamespace, string typeName) = SplitQualifiedIdentifier(typeNameMemory); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
|
||
| using System; | ||
|
|
||
| namespace Microsoft.VisualStudio.Threading.Analyzers; | ||
|
|
||
| /// <summary> | ||
| /// Parsing helpers for additional-file lines used by <see cref="CommonInterest" />. | ||
| /// This class is intentionally kept free of Roslyn dependencies so it can be | ||
| /// linked directly into test projects for unit testing. | ||
| /// </summary> | ||
| internal static class CommonInterestParsing | ||
| { | ||
| /// <summary> | ||
| /// Parses a line that may begin with an optional <c>!</c>, followed by <c>[TypeName]</c>, | ||
| /// and optionally <c>::MemberName</c>, without using regular expressions. | ||
| /// </summary> | ||
| /// <param name="line">The line to parse.</param> | ||
| /// <param name="negated"><see langword="true" /> if the line begins with '!'.</param> | ||
| /// <param name="typeName">The type name parsed from the brackets.</param> | ||
| /// <param name="memberName">The member name after '::', or <see langword="null" /> if not present.</param> | ||
| /// <returns><see langword="true" /> if parsing succeeded.</returns> | ||
| internal static bool TryParseNegatableTypeOrMemberReference(string line, out bool negated, out ReadOnlyMemory<char> typeName, out string? memberName) | ||
| { | ||
| negated = false; | ||
| typeName = default; | ||
| memberName = null; | ||
|
|
||
| ReadOnlySpan<char> span = line.AsSpan(); | ||
| int pos = 0; | ||
|
|
||
| // Optional negation prefix. | ||
| if (pos < span.Length && span[pos] == '!') | ||
| { | ||
| negated = true; | ||
| pos++; | ||
| } | ||
|
|
||
| // Required '[TypeName]'. | ||
| int bracketStart = pos; | ||
| if (!TryParseBracketedTypeName(span, ref pos, out _)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Compute memory slice for type name (between the brackets), using recorded bracket position. | ||
| ReadOnlyMemory<char> typeNameMemory = line.AsMemory(bracketStart + 1, pos - bracketStart - 2); | ||
|
|
||
| // Optional '::memberName'. | ||
| ReadOnlySpan<char> memberNameSpan = default; | ||
| if (pos + 1 < span.Length && span[pos] == ':' && span[pos + 1] == ':') | ||
| { | ||
| pos += 2; | ||
| int memberNameStart = pos; | ||
| while (pos < span.Length && !char.IsWhiteSpace(span[pos])) | ||
| { | ||
| pos++; | ||
| } | ||
|
|
||
| if (pos == memberNameStart) | ||
| { | ||
| // '::' present but no member name follows. | ||
| return false; | ||
| } | ||
|
|
||
| memberNameSpan = span.Slice(memberNameStart, pos - memberNameStart); | ||
| } | ||
|
|
||
| // Allow only trailing whitespace. | ||
| while (pos < span.Length && char.IsWhiteSpace(span[pos])) | ||
| { | ||
| pos++; | ||
| } | ||
|
|
||
| if (pos != span.Length) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Only allocate strings after full validation. | ||
| typeName = typeNameMemory; | ||
| memberName = memberNameSpan.IsEmpty ? null : memberNameSpan.ToString(); | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Parses a line of the form <c>[TypeName]::MemberName</c>, without using regular expressions. | ||
| /// </summary> | ||
| /// <param name="line">The line to parse.</param> | ||
| /// <param name="typeName">The type name parsed from the brackets.</param> | ||
| /// <param name="memberName">The member name after '::'.</param> | ||
| /// <returns><see langword="true" /> if parsing succeeded.</returns> | ||
| internal static bool TryParseMemberReference(string line, out ReadOnlyMemory<char> typeName, out string? memberName) | ||
| { | ||
| typeName = default; | ||
| memberName = null; | ||
|
|
||
| ReadOnlySpan<char> span = line.AsSpan(); | ||
| int pos = 0; | ||
|
|
||
| // Required '[TypeName]'. | ||
| int bracketStart = pos; | ||
| if (!TryParseBracketedTypeName(span, ref pos, out _)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Compute memory slice for type name (between the brackets), using recorded bracket position. | ||
| ReadOnlyMemory<char> typeNameMemory = line.AsMemory(bracketStart + 1, pos - bracketStart - 2); | ||
|
|
||
| // Required '::'. | ||
| if (pos + 1 >= span.Length || span[pos] != ':' || span[pos + 1] != ':') | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| pos += 2; | ||
|
|
||
| // Member name: one or more non-whitespace chars. | ||
| int memberNameStart = pos; | ||
| while (pos < span.Length && !char.IsWhiteSpace(span[pos])) | ||
| { | ||
| pos++; | ||
| } | ||
|
|
||
| if (pos == memberNameStart) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| ReadOnlySpan<char> memberNameSpan = span.Slice(memberNameStart, pos - memberNameStart); | ||
|
|
||
| // Allow only trailing whitespace. | ||
| while (pos < span.Length && char.IsWhiteSpace(span[pos])) | ||
| { | ||
| pos++; | ||
| } | ||
|
|
||
| if (pos != span.Length) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| // Only allocate strings after full validation. | ||
| typeName = typeNameMemory; | ||
| memberName = memberNameSpan.ToString(); | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Advances <paramref name="pos" /> past a <c>[TypeName]</c> token and outputs the type-name span. | ||
| /// </summary> | ||
| /// <param name="span">The full input span.</param> | ||
| /// <param name="pos">The current parse position; advanced past the closing <c>]</c> on success.</param> | ||
| /// <param name="typeName">A slice of <paramref name="span" /> containing the type name, without the brackets.</param> | ||
| /// <returns><see langword="true" /> if a non-empty bracketed type name was consumed.</returns> | ||
| internal static bool TryParseBracketedTypeName(ReadOnlySpan<char> span, ref int pos, out ReadOnlySpan<char> typeName) | ||
| { | ||
| typeName = default; | ||
|
|
||
| // Required opening bracket. | ||
| if (pos >= span.Length || span[pos] != '[') | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| pos++; | ||
|
|
||
| // Type name: one or more chars that are not '[', ']', or ':'. | ||
| int typeNameStart = pos; | ||
| while (pos < span.Length && span[pos] != '[' && span[pos] != ']' && span[pos] != ':') | ||
| { | ||
| pos++; | ||
| } | ||
|
|
||
| if (pos == typeNameStart) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| typeName = span.Slice(typeNameStart, pos - typeNameStart); | ||
|
|
||
| // Required closing bracket. | ||
| if (pos >= span.Length || span[pos] != ']') | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| pos++; | ||
| return true; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.