Skip to content

Commit a56023c

Browse files
committed
Remove slow regex from threading analyzers
1 parent 4332894 commit a56023c

1 file changed

Lines changed: 161 additions & 33 deletions

File tree

src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs

Lines changed: 161 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,6 @@ public static class CommonInterest
6969

7070
private const string GetAwaiterMethodName = "GetAwaiter";
7171

72-
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // Prevent expensive CPU hang in Regex.Match if backtracking occurs due to pathological input (see #485).
73-
74-
private static readonly Regex NegatableTypeOrMemberReferenceRegex = new Regex(@"^(?<negated>!)?\[(?<typeName>[^\[\]\:]+)+\](?:\:\:(?<memberName>\S+))?\s*$", RegexOptions.Singleline | RegexOptions.CultureInvariant, RegexMatchTimeout);
75-
76-
private static readonly Regex MemberReferenceRegex = new Regex(@"^\[(?<typeName>[^\[\]\:]+)+\]::(?<memberName>\S+)\s*$", RegexOptions.Singleline | RegexOptions.CultureInvariant, RegexMatchTimeout);
77-
7872
/// <summary>
7973
/// An array with '.' as its only element.
8074
/// </summary>
@@ -92,28 +86,17 @@ public static IEnumerable<TypeMatchSpec> ReadTypesAndMembers(AnalyzerOptions ana
9286
{
9387
foreach (string line in ReadAdditionalFiles(analyzerOptions, fileNamePattern, cancellationToken))
9488
{
95-
Match? match = null;
96-
try
97-
{
98-
match = NegatableTypeOrMemberReferenceRegex.Match(line);
99-
}
100-
catch (RegexMatchTimeoutException)
101-
{
102-
throw new InvalidOperationException($"Regex.Match timeout when parsing line: {line}");
103-
}
104-
105-
if (!match.Success)
89+
if (!TryParseNegatableTypeOrMemberReference(line, out bool negated, out string? typeNameValue, out string? memberNameValue))
10690
{
10791
throw new InvalidOperationException($"Parsing error on line: {line}");
10892
}
10993

110-
bool inverted = match.Groups["negated"].Success;
111-
string[] typeNameElements = match.Groups["typeName"].Value.Split(QualifiedIdentifierSeparators);
94+
string[] typeNameElements = typeNameValue!.Split(QualifiedIdentifierSeparators);
11295
string typeName = typeNameElements[typeNameElements.Length - 1];
11396
var containingNamespace = typeNameElements.Take(typeNameElements.Length - 1).ToImmutableArray();
11497
var type = new QualifiedType(containingNamespace, typeName);
115-
QualifiedMember member = match.Groups["memberName"].Success ? new QualifiedMember(type, match.Groups["memberName"].Value) : default(QualifiedMember);
116-
yield return new TypeMatchSpec(type, member, inverted);
98+
QualifiedMember member = memberNameValue is not null ? new QualifiedMember(type, memberNameValue) : default(QualifiedMember);
99+
yield return new TypeMatchSpec(type, member, negated);
117100
}
118101
}
119102

@@ -345,26 +328,171 @@ public static IEnumerable<string> ReadLinesFromAdditionalFile(SourceText text)
345328

346329
public static QualifiedMember ParseAdditionalFileMethodLine(string line)
347330
{
348-
Match? match = null;
349-
try
331+
if (!TryParseMemberReference(line, out string? typeNameValue, out string? memberName))
350332
{
351-
match = MemberReferenceRegex.Match(line);
333+
throw new InvalidOperationException($"Parsing error on line: {line}");
352334
}
353-
catch (RegexMatchTimeoutException)
335+
336+
string[] typeNameElements = typeNameValue!.Split(QualifiedIdentifierSeparators);
337+
string typeName = typeNameElements[typeNameElements.Length - 1];
338+
var containingType = new QualifiedType(typeNameElements.Take(typeNameElements.Length - 1).ToImmutableArray(), typeName);
339+
return new QualifiedMember(containingType, memberName!);
340+
}
341+
342+
/// <summary>
343+
/// Parses a line of the form <c>!?[TypeName](::MemberName)?</c> without using regex.
344+
/// </summary>
345+
/// <param name="line">The line to parse.</param>
346+
/// <param name="negated"><see langword="true" /> if the line begins with '!'.</param>
347+
/// <param name="typeName">The type name parsed from the brackets.</param>
348+
/// <param name="memberName">The member name after '::', or <see langword="null" /> if not present.</param>
349+
/// <returns><see langword="true" /> if parsing succeeded.</returns>
350+
private static bool TryParseNegatableTypeOrMemberReference(string line, out bool negated, out string? typeName, out string? memberName)
351+
{
352+
negated = false;
353+
typeName = null;
354+
memberName = null;
355+
356+
int pos = 0;
357+
358+
// Optional negation prefix.
359+
if (pos < line.Length && line[pos] == '!')
354360
{
355-
throw new InvalidOperationException($"Regex.Match timeout when parsing line: {line}");
361+
negated = true;
362+
pos++;
356363
}
357364

358-
if (!match.Success)
365+
// Required opening bracket.
366+
if (pos >= line.Length || line[pos] != '[')
359367
{
360-
throw new InvalidOperationException($"Parsing error on line: {line}");
368+
return false;
361369
}
362370

363-
string methodName = match.Groups["memberName"].Value;
364-
string[] typeNameElements = match.Groups["typeName"].Value.Split(QualifiedIdentifierSeparators);
365-
string typeName = typeNameElements[typeNameElements.Length - 1];
366-
var containingType = new QualifiedType(typeNameElements.Take(typeNameElements.Length - 1).ToImmutableArray(), typeName);
367-
return new QualifiedMember(containingType, methodName);
371+
pos++;
372+
373+
// Type name: one or more chars that are not '[', ']', or ':'.
374+
int typeNameStart = pos;
375+
while (pos < line.Length && line[pos] != '[' && line[pos] != ']' && line[pos] != ':')
376+
{
377+
pos++;
378+
}
379+
380+
if (pos == typeNameStart)
381+
{
382+
return false;
383+
}
384+
385+
typeName = line.Substring(typeNameStart, pos - typeNameStart);
386+
387+
// Required closing bracket.
388+
if (pos >= line.Length || line[pos] != ']')
389+
{
390+
return false;
391+
}
392+
393+
pos++;
394+
395+
// Optional '::memberName'.
396+
if (pos + 1 < line.Length && line[pos] == ':' && line[pos + 1] == ':')
397+
{
398+
pos += 2;
399+
int memberNameStart = pos;
400+
while (pos < line.Length && !char.IsWhiteSpace(line[pos]))
401+
{
402+
pos++;
403+
}
404+
405+
if (pos == memberNameStart)
406+
{
407+
// '::' present but no member name follows.
408+
return false;
409+
}
410+
411+
memberName = line.Substring(memberNameStart, pos - memberNameStart);
412+
}
413+
414+
// Allow only trailing whitespace.
415+
while (pos < line.Length && char.IsWhiteSpace(line[pos]))
416+
{
417+
pos++;
418+
}
419+
420+
return pos == line.Length;
421+
}
422+
423+
/// <summary>
424+
/// Parses a line of the form <c>[TypeName]::MemberName</c> without using regex.
425+
/// </summary>
426+
/// <param name="line">The line to parse.</param>
427+
/// <param name="typeName">The type name parsed from the brackets.</param>
428+
/// <param name="memberName">The member name after '::'.</param>
429+
/// <returns><see langword="true" /> if parsing succeeded.</returns>
430+
private static bool TryParseMemberReference(string line, out string? typeName, out string? memberName)
431+
{
432+
typeName = null;
433+
memberName = null;
434+
435+
int pos = 0;
436+
437+
// Required opening bracket.
438+
if (pos >= line.Length || line[pos] != '[')
439+
{
440+
return false;
441+
}
442+
443+
pos++;
444+
445+
// Type name: one or more chars that are not '[', ']', or ':'.
446+
int typeNameStart = pos;
447+
while (pos < line.Length && line[pos] != '[' && line[pos] != ']' && line[pos] != ':')
448+
{
449+
pos++;
450+
}
451+
452+
if (pos == typeNameStart)
453+
{
454+
return false;
455+
}
456+
457+
typeName = line.Substring(typeNameStart, pos - typeNameStart);
458+
459+
// Required closing bracket.
460+
if (pos >= line.Length || line[pos] != ']')
461+
{
462+
return false;
463+
}
464+
465+
pos++;
466+
467+
// Required '::'.
468+
if (pos + 1 >= line.Length || line[pos] != ':' || line[pos + 1] != ':')
469+
{
470+
return false;
471+
}
472+
473+
pos += 2;
474+
475+
// Member name: one or more non-whitespace chars.
476+
int memberNameStart = pos;
477+
while (pos < line.Length && !char.IsWhiteSpace(line[pos]))
478+
{
479+
pos++;
480+
}
481+
482+
if (pos == memberNameStart)
483+
{
484+
return false;
485+
}
486+
487+
memberName = line.Substring(memberNameStart, pos - memberNameStart);
488+
489+
// Allow only trailing whitespace.
490+
while (pos < line.Length && char.IsWhiteSpace(line[pos]))
491+
{
492+
pos++;
493+
}
494+
495+
return pos == line.Length;
368496
}
369497

370498
private static bool TestGetAwaiterMethod(IMethodSymbol getAwaiterMethod)

0 commit comments

Comments
 (0)