@@ -712,7 +712,7 @@ private static IEnumerable<string> MissingLines(string apiFile, string[] current
712712 var line = oldApiContents[ i] ;
713713 if ( line . Trim ( ) . StartsWith ( "{" ) )
714714 {
715- scopeStack. Add ( oldApiContents [ i - 1 ] ) ;
715+ scopeStack. Add ( i > 0 ? oldApiContents [ i - 1 ] : string . Empty ) ;
716716 }
717717 else if ( line . Trim ( ) . StartsWith ( "}" ) )
718718 {
@@ -724,13 +724,20 @@ private static IEnumerable<string> MissingLines(string apiFile, string[] current
724724 }
725725 }
726726
727+ // Matches hex literals (0xFF).
728+ private static readonly Regex s_HexLiteralRegex =
729+ new Regex( @"\b0x([0-9a-fA-F]+)\b" , RegexOptions . Compiled ) ;
730+ // Matches bitwise shift expressions (1 << 8).
731+ private static readonly Regex s_ShiftExprRegex =
732+ new Regex( @"\b(\d+) << (\d+)\b" , RegexOptions . Compiled ) ;
733+
727734 private static string FilterIgnoredChanges( string line )
728735 {
729736 if ( line . Length == 0 )
730737 return line;
731738
732- // Older API scraper versions emitted fully-qualified C# primitive type names (e.g. System.UInt32),
733- // while newer versions emit C# language aliases (e.g. uint). Normalize to aliases so that a scraper
739+ // Older API scraper versions emitted fully-qualified C# primitive type names (System.UInt32),
740+ // while newer versions emit C# language aliases (uint). Normalize to aliases so that a scraper
734741 // version change does not produce false-positive breaking change reports.
735742 line = line
736743 . Replace ( "System.UInt64" , "ulong" )
@@ -750,30 +757,33 @@ private static string FilterIgnoredChanges(string line)
750757 // Older scrapers resolved expressions to decimal; newer scrapers may keep symbolic forms.
751758 line = line. Replace ( "uint.MaxValue" , "4294967295" )
752759 . Replace ( "uint.MinValue" , "0" ) ;
753- line = Regex. Replace ( line , @"\b0x([0-9a-fA-F]+)\b" ,
754- m => Convert . ToUInt64 ( m . Groups [ 1 ] . Value , 16 ) . ToString ( ) ) ;
755- line = Regex. Replace ( line , @"\b(\d+) << (\d+)\b" ,
760+ // Normalize hex literals (0xFF -> 255).
761+ line = s_HexLiteralRegex. Replace ( line ,
762+ m => Convert . ToUInt64 ( m . Groups [ 1 ] . Value , 16 ) . ToString ( ) ) ;
763+ // Normalize bitwise shift expressions (1 << 8 -> 256).
764+ line = s_ShiftExprRegex. Replace ( line ,
756765 m => ( ulong . Parse ( m . Groups [ 1 ] . Value ) << int . Parse ( m . Groups [ 2 ] . Value ) ) . ToString ( ) ) ;
757766
758767 var pos = 0 ;
759768 while ( true )
760769 {
761770 // Skip whitespace.
762771 while ( pos < line . Length && char . IsWhiteSpace ( line [ pos ] ) )
772+ {
763773 ++ pos;
774+ }
764775
765776 if ( pos >= line . Length || line [ pos ] != '[' )
777+ {
766778 return line;
779+ }
767780
768781 var startPos = pos;
769782 ++ pos;
770783
771784 // Find the matching closing ']' using bracket depth tracking.
772- // This correctly handles new[] syntax in attribute arguments, e.g. :
785+ // This correctly handles new[] syntax in attribute arguments:
773786 // [InputControl(aliases = new[] {@"a", @"b"})] public uint buttons;
774- // The old scraper used Mono.Cecil.CustomAttributeArgument[] (inner ] followed by ,)
775- // but the new scraper uses new[] {...} where the inner ] is followed by a space,
776- // which the old naive scan would incorrectly treat as the end of the attribute.
777787 var depth = 1 ;
778788 while ( pos < line . Length && depth > 0 )
779789 {
@@ -783,15 +793,20 @@ private static string FilterIgnoredChanges(string line)
783793 }
784794
785795 if ( pos >= line . Length )
786- return line; // No matching ']' found, bail out.
796+ {
797+ return line; // No matching ']' found, so out.
798+ }
787799
788800 ++ pos; // Move past the closing ']'.
789801
790- // The attribute must be followed by a space to have any content after it.
802+ // The attribute must be followed by a space.
803+ // If it is the last character there is nothing else to strip, so out.
791804 if ( pos >= line . Length || line [ pos ] != ' ' )
792805 return line;
793806
794- var attributeContent = line. Substring ( startPos + 1 , pos - startPos - 2 ) ;
807+ // Extract the content between '[' and ']'.
808+ var closingBracket = pos - 1 ; // pos is now one past ']'
809+ var attributeContent = line. Substring ( startPos + 1 , closingBracket - startPos - 1 ) ;
795810 if ( ! attributeContent . StartsWith ( "System.Obsolete" ) )
796811 {
797812 line = line. Substring ( 0 , startPos ) + line . Substring ( pos + 1 ) ; // Snip space after ']'.
@@ -829,12 +844,21 @@ public bool IsMatch(List<string> scopeStack, string member)
829844 var namespaceScope = string . Empty;
830845 var typeScope = string . Empty ;
831846
847+ // Walk inside-out so we pick up the innermost namespace and type scopes first.
832848 for ( var i = scopeStack . Count - 1 ; i >= 0 ; i-- )
833849 {
834850 if ( scopeStack [ i ] . StartsWith ( "namespace" ) )
835- namespaceScope = scopeStack [ i ] . Substring ( scopeStack [ i ] . IndexOf ( ' ' ) + 1 ) ;
836- else
851+ {
852+ if ( namespaceScope . Length == 0 )
853+ namespaceScope = scopeStack[ i ] . Substring ( scopeStack [ i ] . IndexOf ( ' ' ) + 1 ) ;
854+ }
855+ else if ( typeScope . Length == 0 )
856+ {
837857 typeScope = scopeStack [ i ] . Trim ( ) ;
858+ }
859+
860+ if ( namespaceScope . Length > 0 && typeScope . Length > 0 )
861+ break;
838862 }
839863
840864 return namespaceScope == Namespace && typeScope == Type && Members . Contains ( member . Trim ( ) ) ;
0 commit comments