Skip to content

Commit

Permalink
Allow semi-colon after record type constraints (dotnet#45943)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcouv committed Jul 22, 2020
1 parent 59d8cd7 commit 41aad25
Show file tree
Hide file tree
Showing 6 changed files with 861 additions and 31 deletions.
14 changes: 13 additions & 1 deletion docs/compilers/CSharp/Compiler Breaking Changes - DotNet 5.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## This document lists known breaking changes in Roslyn in C# 9.0 which will be introduced with .NET 5.
## This document lists known breaking changes in Roslyn in C# 9.0 which will be introduced with .NET 5 (Visual Studio 2019 version 16.8).

1. Beginning with C# 9.0, when you switch on a value of type `byte` or `sbyte`, the compiler tracks which values have been handled and which have not. Technically, we do so for all numeric types, but in practice it is only a breaking change for the types `byte` and `sbyte`. For example, the following program contains a switch statement that explicitly handles *all* of the possible values of the switch's controlling expression
```csharp
Expand Down Expand Up @@ -58,3 +58,15 @@
o is sbyte or short or int or long;
```
Because the `and` and `or` combinators can follow a type pattern, the compiler interprets them as part of the pattern combinator rather than an identifier for the declaration pattern. Consequently, it is an error to use `or` or `and` as pattern variable identifiers starting with C# 9.0.

4. https://github.com/dotnet/roslyn/pull/44841 In *C# 9* and onwards the language views ambiguities between the `record` identifier as being
either a type syntax or a record declaration as choosing the record declaration. The following examples will now be record declarations:

```C#
abstract class C
{
record R2() { }
abstract record R3();
}
```

11 changes: 0 additions & 11 deletions docs/compilers/CSharp/Compiler Breaking Changes - post VS2019.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,3 @@ public class Derived : Base<string?>
public override string? M() { ... } // Derived.M doesn't honor the nullability declaration made by Base.M with its [NotNull] attribute
}
```

19. https://github.com/dotnet/roslyn/pull/44841 In *C# 9* and onwards the language views ambiguities between the `record` identifier as being
either a type syntax or a record declaration as choosing the record declaration. The following examples will now be record declarations:

```C#
abstract class C
{
record R2() { }
abstract record R3();
}
```
23 changes: 20 additions & 3 deletions src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ internal enum TerminatorState
IsEndOfNameInExplicitInterface = 1 << 22,
IsEndOfFunctionPointerParameterList = 1 << 23,
IsEndOfFunctionPointerParameterListErrored = 1 << 24,
IsEndOfRecordSignature = 1 << 25,
}

private const int LastTerminatorState = (int)TerminatorState.IsEndOfFunctionPointerParameterListErrored;
private const int LastTerminatorState = (int)TerminatorState.IsEndOfRecordSignature;

private bool IsTerminator()
{
Expand Down Expand Up @@ -124,6 +125,7 @@ private bool IsTerminator()
case TerminatorState.IsEndOfNameInExplicitInterface when this.IsEndOfNameInExplicitInterface():
case TerminatorState.IsEndOfFunctionPointerParameterList when this.IsEndOfFunctionPointerParameterList(errored: false):
case TerminatorState.IsEndOfFunctionPointerParameterListErrored when this.IsEndOfFunctionPointerParameterList(errored: true):
case TerminatorState.IsEndOfRecordSignature when this.IsEndOfRecordSignature():
return true;
}
}
Expand Down Expand Up @@ -1448,8 +1450,15 @@ private TypeDeclarationSyntax ParseClassOrStructOrInterfaceDeclaration(SyntaxLis

var keyword = ConvertToKeyword(this.EatToken());

var outerSaveTerm = _termState;
if (keyword.Kind == SyntaxKind.RecordKeyword)
{
_termState |= TerminatorState.IsEndOfRecordSignature;
}

var saveTerm = _termState;
_termState |= TerminatorState.IsPossibleAggregateClauseStartOrStop;

var name = this.ParseIdentifierToken();
var typeParameters = this.ParseTypeParameterList();

Expand All @@ -1471,6 +1480,8 @@ private TypeDeclarationSyntax ParseClassOrStructOrInterfaceDeclaration(SyntaxLis
this.ParseTypeParameterConstraintClauses(constraints);
}

_termState = outerSaveTerm;

SyntaxToken semicolon;
SyntaxToken? openBrace;
SyntaxToken? closeBrace;
Expand Down Expand Up @@ -1759,7 +1770,7 @@ private BaseListSyntax ParseBaseList(SyntaxToken typeKeyword, bool haveParameter
while (true)
{
if (this.CurrentToken.Kind == SyntaxKind.OpenBraceToken ||
this.CurrentToken.Kind == SyntaxKind.SemicolonToken ||
((_termState & TerminatorState.IsEndOfRecordSignature) != 0 && this.CurrentToken.Kind == SyntaxKind.SemicolonToken) ||
this.IsCurrentTokenWhereOfConstraintClause())
{
break;
Expand Down Expand Up @@ -1788,7 +1799,7 @@ private PostSkipAction SkipBadBaseListTokens(ref SyntaxToken colon, SeparatedSyn
{
return this.SkipBadSeparatedListTokensWithExpectedKind(ref colon, list,
p => p.CurrentToken.Kind != SyntaxKind.CommaToken && !p.IsPossibleAttribute(),
p => p.CurrentToken.Kind == SyntaxKind.SemicolonToken || p.CurrentToken.Kind == SyntaxKind.OpenBraceToken || p.IsCurrentTokenWhereOfConstraintClause() || p.IsTerminator(),
p => p.CurrentToken.Kind == SyntaxKind.OpenBraceToken || p.IsCurrentTokenWhereOfConstraintClause() || p.IsTerminator(),
expected);
}

Expand Down Expand Up @@ -1833,6 +1844,7 @@ private TypeParameterConstraintClauseSyntax ParseTypeParameterConstraintClause()
while (true)
{
if (this.CurrentToken.Kind == SyntaxKind.OpenBraceToken
|| ((_termState & TerminatorState.IsEndOfRecordSignature) != 0 && this.CurrentToken.Kind == SyntaxKind.SemicolonToken)
|| this.CurrentToken.Kind == SyntaxKind.EqualsGreaterThanToken
|| this.CurrentToken.ContextualKind == SyntaxKind.WhereKeyword)
{
Expand Down Expand Up @@ -3024,6 +3036,11 @@ private bool IsEndOfMethodSignature()
return this.CurrentToken.Kind == SyntaxKind.SemicolonToken || this.CurrentToken.Kind == SyntaxKind.OpenBraceToken;
}

private bool IsEndOfRecordSignature()
{
return this.CurrentToken.Kind == SyntaxKind.SemicolonToken || this.CurrentToken.Kind == SyntaxKind.OpenBraceToken;
}

private bool IsEndOfNameInExplicitInterface()
{
return this.CurrentToken.Kind == SyntaxKind.DotToken || this.CurrentToken.Kind == SyntaxKind.ColonColonToken;
Expand Down
12 changes: 6 additions & 6 deletions src/Compilers/CSharp/Test/Emit/Emit/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ public void DeeplyNestedGeneric()
int nestingLevel = (ExecutionConditionUtil.Architecture, ExecutionConditionUtil.Configuration) switch
{
// Legacy baselines are indicated by comments
(ExecutionArchitecture.x64, ExecutionConfiguration.Debug) when ExecutionConditionUtil.IsMacOS => 200, // 100
(ExecutionArchitecture.x64, ExecutionConfiguration.Debug) when ExecutionConditionUtil.IsMacOS => 180, // 100
(ExecutionArchitecture.x64, ExecutionConfiguration.Release) when ExecutionConditionUtil.IsMacOS => 520, // 100
_ when ExecutionConditionUtil.IsCoreClrUnix => 1200, // 1200
_ when ExecutionConditionUtil.IsMonoDesktop => 730, // 730
(ExecutionArchitecture.x86, ExecutionConfiguration.Debug) => 460, // 270
(ExecutionArchitecture.x86, ExecutionConfiguration.Debug) => 450, // 270
(ExecutionArchitecture.x86, ExecutionConfiguration.Release) => 1290, // 1290
(ExecutionArchitecture.x64, ExecutionConfiguration.Debug) => 260, // 170
(ExecutionArchitecture.x64, ExecutionConfiguration.Debug) => 250, // 170
(ExecutionArchitecture.x64, ExecutionConfiguration.Release) => 730, // 730
_ => throw new Exception($"Unexpected configuration {ExecutionConditionUtil.Architecture} {ExecutionConditionUtil.Configuration}")
};
Expand Down Expand Up @@ -217,7 +217,7 @@ public static void Main(string[] args)
var source = builder.ToString();
RunInThread(() =>
{
var compilation = CreateCompilation(source, options: TestOptions.DebugExe);
var compilation = CreateCompilation(source, options: TestOptions.DebugExe.WithConcurrentBuild(false));
compilation.VerifyDiagnostics();
// PEVerify is skipped here as it doesn't scale to this level of nested generics. After
Expand Down Expand Up @@ -266,7 +266,7 @@ static void Main()
var source = builder.ToString();
RunInThread(() =>
{
var comp = CreateCompilation(source);
var comp = CreateCompilation(source, options: TestOptions.DebugDll.WithConcurrentBuild(false));
comp.VerifyDiagnostics();
});
}
Expand Down Expand Up @@ -306,7 +306,7 @@ static void runTest(int n)

RunInThread(() =>
{
var comp = CreateCompilation(source);
var comp = CreateCompilation(source, options: TestOptions.DebugDll.WithConcurrentBuild(false));
var type = comp.GetMember<NamedTypeSymbol>("C0");
var typeParameter = type.TypeParameters[0];
Assert.True(typeParameter.IsReferenceType);
Expand Down
57 changes: 57 additions & 0 deletions src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18572,5 +18572,62 @@ protected C(C<T> other)
Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "P2").WithLocation(10, 15)
);
}

[Fact]
public void RecordWithConstraints_NullableWarning()
{
var src = @"
#nullable enable
record R<T>(T P) where T : class;
record R2<T>(T P) where T : class { }

public class C
{
public static void Main()
{
var r = new R<string?>(""R"");
var r2 = new R2<string?>(""R2"");
System.Console.Write((r.P, r2.P));
}
}";

var comp = CreateCompilation(new[] { src, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.DebugExe);
comp.VerifyDiagnostics(
// (10,23): warning CS8634: The type 'string?' cannot be used as type parameter 'T' in the generic type or method 'R<T>'. Nullability of type argument 'string?' doesn't match 'class' constraint.
// var r = new R<string?>("R");
Diagnostic(ErrorCode.WRN_NullabilityMismatchInTypeParameterReferenceTypeConstraint, "string?").WithArguments("R<T>", "T", "string?").WithLocation(10, 23),
// (11,25): warning CS8634: The type 'string?' cannot be used as type parameter 'T' in the generic type or method 'R2<T>'. Nullability of type argument 'string?' doesn't match 'class' constraint.
// var r2 = new R2<string?>("R2");
Diagnostic(ErrorCode.WRN_NullabilityMismatchInTypeParameterReferenceTypeConstraint, "string?").WithArguments("R2<T>", "T", "string?").WithLocation(11, 25)
);
CompileAndVerify(comp, expectedOutput: "(R, R2)", verify: Verification.Skipped /* init-only */);
}

[Fact]
public void RecordWithConstraints_ConstraintError()
{
var src = @"
record R<T>(T P) where T : class;
record R2<T>(T P) where T : class { }

public class C
{
public static void Main()
{
_ = new R<int>(1);
_ = new R2<int>(2);
}
}";

var comp = CreateCompilation(src);
comp.VerifyDiagnostics(
// (9,19): error CS0452: The type 'int' must be a reference type in order to use it as parameter 'T' in the generic type or method 'R<T>'
// _ = new R<int>(1);
Diagnostic(ErrorCode.ERR_RefConstraintNotSatisfied, "int").WithArguments("R<T>", "T", "int").WithLocation(9, 19),
// (10,20): error CS0452: The type 'int' must be a reference type in order to use it as parameter 'T' in the generic type or method 'R2<T>'
// _ = new R2<int>(2);
Diagnostic(ErrorCode.ERR_RefConstraintNotSatisfied, "int").WithArguments("R2<T>", "T", "int").WithLocation(10, 20)
);
}
}
}
Loading

0 comments on commit 41aad25

Please sign in to comment.