@@ -709,6 +709,7 @@ public static ImmutableArray<CSharpDirective> FindDirectives(SourceFile sourceFi
709
709
{
710
710
#pragma warning disable RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
711
711
712
+ var deduplicated = new HashSet < CSharpDirective . Named > ( NamedDirectiveComparer . Instance ) ;
712
713
var builder = ImmutableArray . CreateBuilder < CSharpDirective > ( ) ;
713
714
SyntaxTokenParser tokenizer = SyntaxFactory . CreateTokenParser ( sourceFile . Text ,
714
715
CSharpParseOptions . Default . WithFeatures ( [ new ( "FileBasedProgram" , "true" ) ] ) ) ;
@@ -750,6 +751,28 @@ public static ImmutableArray<CSharpDirective> FindDirectives(SourceFile sourceFi
750
751
751
752
if ( CSharpDirective . Parse ( errors , sourceFile , span , name . ToString ( ) , value . ToString ( ) ) is { } directive )
752
753
{
754
+ // If the directive is already present, report an error.
755
+ if ( deduplicated . TryGetValue ( directive , out var existingDirective ) )
756
+ {
757
+ var typeAndName = $ "#:{ existingDirective . GetType ( ) . Name . ToLowerInvariant ( ) } { existingDirective . Name } ";
758
+ if ( errors != null )
759
+ {
760
+ errors . Add ( new SimpleDiagnostic
761
+ {
762
+ Location = sourceFile . GetFileLinePositionSpan ( directive . Span ) ,
763
+ Message = string . Format ( CliCommandStrings . DuplicateDirective , typeAndName , sourceFile . GetLocationString ( directive . Span ) ) ,
764
+ } ) ;
765
+ }
766
+ else
767
+ {
768
+ throw new GracefulException ( CliCommandStrings . DuplicateDirective , typeAndName , sourceFile . GetLocationString ( directive . Span ) ) ;
769
+ }
770
+ }
771
+ else
772
+ {
773
+ deduplicated . Add ( directive ) ;
774
+ }
775
+
753
776
builder . Add ( directive ) ;
754
777
}
755
778
}
@@ -872,7 +895,8 @@ internal static partial class Patterns
872
895
}
873
896
874
897
/// <summary>
875
- /// Represents a C# directive starting with <c>#:</c>. Those are ignored by the language but recognized by us.
898
+ /// Represents a C# directive starting with <c>#:</c> (a.k.a., "file-level directive").
899
+ /// Those are ignored by the language but recognized by us.
876
900
/// </summary>
877
901
internal abstract class CSharpDirective
878
902
{
@@ -883,14 +907,14 @@ private CSharpDirective() { }
883
907
/// </summary>
884
908
public required TextSpan Span { get ; init ; }
885
909
886
- public static CSharpDirective ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
910
+ public static Named ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
887
911
{
888
912
return directiveKind switch
889
913
{
890
914
"sdk" => Sdk . Parse ( errors , sourceFile , span , directiveKind , directiveText ) ,
891
915
"property" => Property . Parse ( errors , sourceFile , span , directiveKind , directiveText ) ,
892
916
"package" => Package . Parse ( errors , sourceFile , span , directiveKind , directiveText ) ,
893
- _ => ReportError < CSharpDirective > ( errors , sourceFile , span , string . Format ( CliCommandStrings . UnrecognizedDirective , directiveKind , sourceFile . GetLocationString ( span ) ) ) ,
917
+ _ => ReportError < Named > ( errors , sourceFile , span , string . Format ( CliCommandStrings . UnrecognizedDirective , directiveKind , sourceFile . GetLocationString ( span ) ) ) ,
894
918
} ;
895
919
}
896
920
@@ -933,14 +957,18 @@ private static (string, string?)? ParseOptionalTwoParts(ImmutableArray<SimpleDia
933
957
/// </summary>
934
958
public sealed class Shebang : CSharpDirective ;
935
959
960
+ public abstract class Named : CSharpDirective
961
+ {
962
+ public required string Name { get ; init ; }
963
+ }
964
+
936
965
/// <summary>
937
966
/// <c>#:sdk</c> directive.
938
967
/// </summary>
939
- public sealed class Sdk : CSharpDirective
968
+ public sealed class Sdk : Named
940
969
{
941
970
private Sdk ( ) { }
942
971
943
- public required string Name { get ; init ; }
944
972
public string ? Version { get ; init ; }
945
973
946
974
public static new Sdk ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
@@ -967,11 +995,10 @@ public string ToSlashDelimitedString()
967
995
/// <summary>
968
996
/// <c>#:property</c> directive.
969
997
/// </summary>
970
- public sealed class Property : CSharpDirective
998
+ public sealed class Property : Named
971
999
{
972
1000
private Property ( ) { }
973
1001
974
- public required string Name { get ; init ; }
975
1002
public required string Value { get ; init ; }
976
1003
977
1004
public static new Property ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
@@ -1007,13 +1034,12 @@ private Property() { }
1007
1034
/// <summary>
1008
1035
/// <c>#:package</c> directive.
1009
1036
/// </summary>
1010
- public sealed class Package : CSharpDirective
1037
+ public sealed class Package : Named
1011
1038
{
1012
1039
private static readonly SearchValues < char > s_separators = SearchValues . Create ( ' ' , '@' ) ;
1013
1040
1014
1041
private Package ( ) { }
1015
1042
1016
- public required string Name { get ; init ; }
1017
1043
public string ? Version { get ; init ; }
1018
1044
1019
1045
public static new Package ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
@@ -1033,6 +1059,33 @@ private Package() { }
1033
1059
}
1034
1060
}
1035
1061
1062
+ /// <summary>
1063
+ /// Used for deduplication - compares directives by their type and name (ignoring case).
1064
+ /// </summary>
1065
+ internal sealed class NamedDirectiveComparer : IEqualityComparer < CSharpDirective . Named >
1066
+ {
1067
+ public static readonly NamedDirectiveComparer Instance = new ( ) ;
1068
+
1069
+ private NamedDirectiveComparer ( ) { }
1070
+
1071
+ public bool Equals ( CSharpDirective . Named ? x , CSharpDirective . Named ? y )
1072
+ {
1073
+ if ( ReferenceEquals ( x , y ) ) return true ;
1074
+
1075
+ if ( x is null || y is null ) return false ;
1076
+
1077
+ return x . GetType ( ) == y . GetType ( ) &&
1078
+ string . Equals ( x . Name , y . Name , StringComparison . OrdinalIgnoreCase ) ;
1079
+ }
1080
+
1081
+ public int GetHashCode ( CSharpDirective . Named obj )
1082
+ {
1083
+ return HashCode . Combine (
1084
+ obj . GetType ( ) . GetHashCode ( ) ,
1085
+ obj . Name . GetHashCode ( StringComparison . OrdinalIgnoreCase ) ) ;
1086
+ }
1087
+ }
1088
+
1036
1089
internal sealed class SimpleDiagnostic
1037
1090
{
1038
1091
public required Position Location { get ; init ; }
0 commit comments