Skip to content

Commit a0ba641

Browse files
committed
Disallow duplicate file-level directives
1 parent 47d0bba commit a0ba641

16 files changed

+228
-27
lines changed

src/Cli/dotnet/Commands/CliCommandStrings.resx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,10 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
15131513
<value>Some directives cannot be converted: the first error is at {0}. Run the file to see all compilation errors. Specify '--force' to convert anyway.</value>
15141514
<comment>{Locked="--force"}. {0} is the file path and line number.</comment>
15151515
</data>
1516+
<data name="DuplicateDirective" xml:space="preserve">
1517+
<value>Duplicate directives are not supported: {0} at {1}</value>
1518+
<comment>{0} is the directive type and name. {1} is the file path and line number.</comment>
1519+
</data>
15161520
<data name="InvalidOptionCombination" xml:space="preserve">
15171521
<value>Cannot combine option '{0}' and '{1}'.</value>
15181522
<comment>{0} and {1} are option names like '--no-build'.</comment>

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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>
877901
internal 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+
10361089
internal sealed class SimpleDiagnostic
10371090
{
10381091
public required Position Location { get; init; }

src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)