@@ -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