@@ -709,6 +709,7 @@ public static ImmutableArray<CSharpDirective> FindDirectives(SourceFile sourceFi
709709 {
710710#pragma warning disable RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
711711
712+ var deduplicated = new HashSet < CSharpDirective . Named > ( NamedDirectiveComparer . Instance ) ;
712713 var builder = ImmutableArray . CreateBuilder < CSharpDirective > ( ) ;
713714 SyntaxTokenParser tokenizer = SyntaxFactory . CreateTokenParser ( sourceFile . Text ,
714715 CSharpParseOptions . Default . WithFeatures ( [ new ( "FileBasedProgram" , "true" ) ] ) ) ;
@@ -750,6 +751,28 @@ public static ImmutableArray<CSharpDirective> FindDirectives(SourceFile sourceFi
750751
751752 if ( CSharpDirective . Parse ( errors , sourceFile , span , name . ToString ( ) , value . ToString ( ) ) is { } directive )
752753 {
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+
753776 builder . Add ( directive ) ;
754777 }
755778 }
@@ -872,7 +895,8 @@ internal static partial class Patterns
872895}
873896
874897/// <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.
876900/// </summary>
877901internal abstract class CSharpDirective
878902{
@@ -883,14 +907,14 @@ private CSharpDirective() { }
883907 /// </summary>
884908 public required TextSpan Span { get ; init ; }
885909
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 )
887911 {
888912 return directiveKind switch
889913 {
890914 "sdk" => Sdk . Parse ( errors , sourceFile , span , directiveKind , directiveText ) ,
891915 "property" => Property . Parse ( errors , sourceFile , span , directiveKind , directiveText ) ,
892916 "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 ) ) ) ,
894918 } ;
895919 }
896920
@@ -933,14 +957,18 @@ private static (string, string?)? ParseOptionalTwoParts(ImmutableArray<SimpleDia
933957 /// </summary>
934958 public sealed class Shebang : CSharpDirective ;
935959
960+ public abstract class Named : CSharpDirective
961+ {
962+ public required string Name { get ; init ; }
963+ }
964+
936965 /// <summary>
937966 /// <c>#:sdk</c> directive.
938967 /// </summary>
939- public sealed class Sdk : CSharpDirective
968+ public sealed class Sdk : Named
940969 {
941970 private Sdk ( ) { }
942971
943- public required string Name { get ; init ; }
944972 public string ? Version { get ; init ; }
945973
946974 public static new Sdk ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
@@ -967,11 +995,10 @@ public string ToSlashDelimitedString()
967995 /// <summary>
968996 /// <c>#:property</c> directive.
969997 /// </summary>
970- public sealed class Property : CSharpDirective
998+ public sealed class Property : Named
971999 {
9721000 private Property ( ) { }
9731001
974- public required string Name { get ; init ; }
9751002 public required string Value { get ; init ; }
9761003
9771004 public static new Property ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
@@ -1007,13 +1034,12 @@ private Property() { }
10071034 /// <summary>
10081035 /// <c>#:package</c> directive.
10091036 /// </summary>
1010- public sealed class Package : CSharpDirective
1037+ public sealed class Package : Named
10111038 {
10121039 private static readonly SearchValues < char > s_separators = SearchValues . Create ( ' ' , '@' ) ;
10131040
10141041 private Package ( ) { }
10151042
1016- public required string Name { get ; init ; }
10171043 public string ? Version { get ; init ; }
10181044
10191045 public static new Package ? Parse ( ImmutableArray < SimpleDiagnostic > . Builder ? errors , SourceFile sourceFile , TextSpan span , string directiveKind , string directiveText )
@@ -1033,6 +1059,33 @@ private Package() { }
10331059 }
10341060}
10351061
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+
10361089internal sealed class SimpleDiagnostic
10371090{
10381091 public required Position Location { get ; init ; }
0 commit comments