Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add public IsInitOnly API and use it to fix code generation of 'init' #44077

Merged
merged 10 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,7 @@ public override void VisitProperty(IPropertySymbol symbol)

private static bool IsInitOnly(IMethodSymbol symbol)
{
// PROTOTYPE(init-only): adjust SymbolDisplayVisitor once we have a public IsInitOnly API
return (symbol as Symbols.PublicModel.MethodSymbol)?.UnderlyingMethodSymbol.IsInitOnly == true;
return symbol?.IsInitOnly == true;
}

private void AddPropertyNameAndParameters(IPropertySymbol symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ bool IMethodSymbol.IsReadOnly
}
}

bool IMethodSymbol.IsInitOnly
{
get
{
return _underlying.IsInitOnly;
}
}

IMethodSymbol IMethodSymbol.OriginalDefinition
{
get
Expand Down
240 changes: 234 additions & 6 deletions src/Compilers/CSharp/Test/Semantic/Semantics/InitOnlyMemberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ public class InitOnlyMemberTests : CompilingTestBase
// Spec: https://github.com/dotnet/csharplang/blob/master/proposals/init.md

// PROTOTYPE(init-only): test allowed from 'with' expression
// PROTOTYPE(init-only): public API, confirm behavior of IsInitOnly and test on each leaf type of wrapped symbol

// PROTOTYPE(init-only): open issues:
// PROTOTYPE(init-only): queue discussion on init methods (`init void Init()`) and collection initializers (`init void Add()`)

// PROTOTYPE(init-only): test dynamic scenario
// PROTOTYPE(init-only): test whether reflection use property despite modreq?
// PROTOTYPE(init-only): test behavior of old compiler with modreq. For example VB
Expand All @@ -48,6 +43,9 @@ public class C
var property = (PropertySymbol)comp.GlobalNamespace.GetMember("C.Property");
Assert.False(property.GetMethod.IsInitOnly);
Assert.True(property.SetMethod.IsInitOnly);
IPropertySymbol publicProperty = property.GetPublicSymbol();
Assert.False(publicProperty.GetMethod.IsInitOnly);
Assert.True(publicProperty.SetMethod.IsInitOnly);
}

[Fact]
Expand Down Expand Up @@ -108,12 +106,15 @@ public class C

var property = (PropertySymbol)comp.GlobalNamespace.GetMember("C.Property");
Assert.False(property.SetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().SetMethod.IsInitOnly);

var property2 = (PropertySymbol)comp.GlobalNamespace.GetMember("C.Property2");
Assert.True(property2.SetMethod.IsInitOnly);
Assert.True(property2.GetPublicSymbol().SetMethod.IsInitOnly);

var property3 = (PropertySymbol)comp.GlobalNamespace.GetMember("C.Property3");
Assert.True(property3.SetMethod.IsInitOnly);
Assert.True(property3.GetPublicSymbol().SetMethod.IsInitOnly);
}

[Fact]
Expand Down Expand Up @@ -302,7 +303,9 @@ void M()

var property = (PropertySymbol)comp.GlobalNamespace.GetTypeMember("Derived").BaseTypeNoUseSiteDiagnostics.GetMember("Property");
Assert.False(property.GetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().GetMethod.IsInitOnly);
Assert.True(property.SetMethod.IsInitOnly);
Assert.True(property.GetPublicSymbol().SetMethod.IsInitOnly);
}

[Fact]
Expand Down Expand Up @@ -331,7 +334,9 @@ public class CWithoutInit : I<string> // 1

var property = (PropertySymbol)comp.GlobalNamespace.GetMember("I.Property");
Assert.False(property.GetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().GetMethod.IsInitOnly);
Assert.True(property.SetMethod.IsInitOnly);
Assert.True(property.GetPublicSymbol().SetMethod.IsInitOnly);
}

[Fact]
Expand Down Expand Up @@ -588,7 +593,77 @@ public string RegularProperty2

var property = (PropertySymbol)comp.GlobalNamespace.GetMember("C.Property");
Assert.False(property.GetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().GetMethod.IsInitOnly);
Assert.True(property.SetMethod.IsInitOnly);
Assert.True(property.GetPublicSymbol().SetMethod.IsInitOnly);
}

[Fact(Skip = "PROTOTYPE(init-only) Not yet supported")]
public void InitOnlyPropertyAssignmentAllowedInWithInitializer()
{
string source = @"
public class C
{
public int Property { get; init; }

void M(C c)
{
_ = c with { Property = null };
}

public C Clone() => throw null;
}

class Derived : C
{
}

class Derived2 : Derived
{
void M(C c)
{
_ = c with { Property = null };
_ = this with { Property = null };
}
}

class Other
{
void M()
{
var c = new C() with { Property = 42 };
System.Console.Write($""{c.Property}"");
}
}
";
var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview);
comp.VerifyDiagnostics();
}

[Fact(Skip = "PROTOTYPE(init-only) Not yet supported")]
public void InitOnlyPropertyAssignmentAllowedInWithInitializer_Evaluation()
{
string source = @"
public class C
{
private int field;
public int Property { get { return field; } init { field = value; System.Console.Write(""set ""); } }

public C Clone() { System.Console.Write(""clone ""); return this; }
}

class Other
{
public static void Main()
{
var c = new C() with { Property = 42 };
System.Console.Write($""{c.Property}"");
}
}
";
var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview, options: TestOptions.DebugExe);
comp.VerifyDiagnostics();
CompileAndVerify(comp, expectedOutput: "clone set 42");
}

[Fact]
Expand Down Expand Up @@ -773,8 +848,9 @@ public class C

var property = (PropertySymbol)comp.GlobalNamespace.GetMember("C.Property");
Assert.False(property.GetMethod.IsInitOnly);
// PROTOTYPE(init-only): confirm during public API discussion for IsInitOnly
Assert.False(property.GetPublicSymbol().GetMethod.IsInitOnly);
Assert.False(property.SetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().SetMethod.IsInitOnly);
}

[Fact]
Expand Down Expand Up @@ -1075,6 +1151,7 @@ void symbolValidator(ModuleSymbol m)
var getter = property.GetMethod;
Assert.Empty(property.GetMethod.ReturnTypeWithAnnotations.CustomModifiers);
Assert.False(getter.IsInitOnly);
Assert.False(getter.GetPublicSymbol().IsInitOnly);
var getterAttributes = getter.GetAttributes().Select(a => a.ToString());
if (isSource)
{
Expand All @@ -1087,6 +1164,7 @@ void symbolValidator(ModuleSymbol m)

var setter = property.SetMethod;
Assert.True(setter.IsInitOnly);
Assert.True(setter.GetPublicSymbol().IsInitOnly);
var setterAttributes = property.SetMethod.GetAttributes().Select(a => a.ToString());
var modifier = property.SetMethod.ReturnTypeWithAnnotations.CustomModifiers.Single();
Assert.Equal("System.Runtime.CompilerServices.IsExternalInit", modifier.Modifier.ToTestDisplayString());
Expand Down Expand Up @@ -1986,6 +2064,111 @@ public event System.Action Event
});
}

[Fact]
public void EventAccessorsAreNotInitOnly()
{
string source = @"
public class C
{
public event System.Action Event
{
add { }
remove { }
}
}
";
var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview);
comp.VerifyDiagnostics();

var eventSymbol = comp.GlobalNamespace.GetMember<EventSymbol>("C.Event");
Assert.False(eventSymbol.AddMethod.IsInitOnly);
Assert.False(eventSymbol.GetPublicSymbol().AddMethod.IsInitOnly);
Assert.False(eventSymbol.RemoveMethod.IsInitOnly);
Assert.False(eventSymbol.GetPublicSymbol().RemoveMethod.IsInitOnly);
}

[Fact]
public void ConstructorAndDestructorAreNotInitOnly()
Copy link
Contributor

@AlekseyTs AlekseyTs May 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstructorAndDestructorAreNotInitOnly [](start = 20, length = 38)

Could also cover operators #Closed

{
string source = @"
public class C
{
public C() { }
~C() { }
}
";
var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview);
comp.VerifyDiagnostics();

var constructor = comp.GlobalNamespace.GetMember<SourceConstructorSymbol>("C..ctor");
Assert.False(constructor.IsInitOnly);
Assert.False(constructor.GetPublicSymbol().IsInitOnly);

var destructor = comp.GlobalNamespace.GetMember<SourceDestructorSymbol>("C.Finalize");
Assert.False(destructor.IsInitOnly);
Assert.False(destructor.GetPublicSymbol().IsInitOnly);
}

[Fact]
public void ConstructedMethodsAreNotInitOnly()
{
string source = @"
public class C
{
void M<T>()
{
M<string>();
}
}
";
var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview);
comp.VerifyDiagnostics();

var tree = comp.SyntaxTrees[0];
var root = tree.GetCompilationUnitRoot();
var model = comp.GetSemanticModel(tree, ignoreAccessibility: true);

var invocation = root.DescendantNodes().OfType<InvocationExpressionSyntax>().Single();
var method = (IMethodSymbol)model.GetSymbolInfo(invocation).Symbol;
Assert.Equal("void C.M<System.String>()", method.ToTestDisplayString());
Assert.False(method.IsInitOnly);
}

[Fact]
public void InitOnlyOnMembersOfRecords()
{
string source = @"
public data class C(int i)
{
void M() { }
}
";
var comp = CreateCompilation(new[] { source, IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview);
comp.VerifyDiagnostics();

var cMembers = comp.GlobalNamespace.GetMember<NamedTypeSymbol>("C").GetMembers();
// PROTOTYPE(init-only): expecting an 'init' setter
AssertEx.SetEqual(new[] {
"System.Int32 C.<i>k__BackingField",
"System.Int32 C.i.get",
"System.Int32 C.i { get; }",
"void C.M()",
"System.Boolean C.Equals(C? )",
"System.Boolean C.Equals(System.Object? )",
"System.Int32 C.GetHashCode()",
"C..ctor(System.Int32 i)" }, cMembers.ToTestDisplayStrings());

foreach (var member in cMembers)
{
if (member is MethodSymbol method)
{
bool isSetter = method.MethodKind == MethodKind.PropertySet;
Assert.Equal(isSetter, method.IsInitOnly);
Assert.Equal(isSetter, method.GetPublicSymbol().IsInitOnly);
}
}
}

[Fact]
public void IndexerWithInitOnly()
{
Expand Down Expand Up @@ -2897,6 +3080,7 @@ void M2()
// PROTOTYPE(init-only): decoding should be more restrictive
var method = (PEMethodSymbol)comp.GlobalNamespace.GetMember("C.M");
Assert.False(method.IsInitOnly);
Assert.False(method.GetPublicSymbol().IsInitOnly);
Assert.False(method.HasUseSiteError); // PROTOTYPE(init-only): expect true
Assert.False(method.ReturnType.IsErrorType()); // PROTOTYPE(init-only): expect true
}
Expand Down Expand Up @@ -3023,6 +3207,7 @@ void M(C c, ref int i)
Assert.False(property0.GetMethod.HasUseSiteError);
Assert.True(property0.SetMethod.HasUseSiteError);
Assert.False(property0.SetMethod.IsInitOnly);
Assert.False(property0.GetPublicSymbol().SetMethod.IsInitOnly);
}

[Fact]
Expand Down Expand Up @@ -3108,6 +3293,7 @@ void M(C c, ref int i)

Assert.True(property0.SetMethod.HasUseSiteError);
Assert.False(property0.SetMethod.IsInitOnly);
Assert.False(property0.GetPublicSymbol().SetMethod.IsInitOnly);
Assert.True(property0.SetMethod.Parameters[0].Type.IsErrorType());
}

Expand Down Expand Up @@ -3196,6 +3382,7 @@ void M(C c, ref int i)

Assert.True(property0.SetMethod.HasUseSiteError);
Assert.False(property0.SetMethod.IsInitOnly);
Assert.False(property0.GetPublicSymbol().SetMethod.IsInitOnly);
Assert.True(property0.SetMethod.Parameters[0].Type.IsErrorType());
}

Expand Down Expand Up @@ -3255,8 +3442,10 @@ public class Derived : C
// PROTOTYPE(init-only): getter should have use-site error
var property = (PEPropertySymbol)comp.GlobalNamespace.GetMember("C.Property");
Assert.False(property.GetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().GetMethod.IsInitOnly);
Assert.False(property.GetMethod.HasUseSiteError);
Assert.False(property.SetMethod.IsInitOnly);
Assert.False(property.GetPublicSymbol().SetMethod.IsInitOnly);
Assert.False(property.SetMethod.HasUseSiteError);
}

Expand Down Expand Up @@ -3309,5 +3498,44 @@ static D()
Diagnostic(ErrorCode.ERR_BaseInStaticMeth, "base").WithLocation(17, 9)
);
}

[Fact]
public void LocalFunctionsAreNotInitOnly()
{
var comp = CreateCompilation(new[] { @"
public class C
{
delegate void Delegate();

void M()
{
local();
void local() { }
}
}
", IsExternalInitTypeDefinition }, parseOptions: TestOptions.RegularPreview);

comp.VerifyDiagnostics();

var tree = comp.SyntaxTrees[0];
var model = comp.GetSemanticModel(tree);

var localFunctionSyntax = tree.GetRoot().DescendantNodes().OfType<LocalFunctionStatementSyntax>().Single();
var localFunctionSymbol = model.GetDeclaredSymbol(localFunctionSyntax).GetSymbol<LocalFunctionSymbol>();
Assert.False(localFunctionSymbol.IsInitOnly);
Assert.False(localFunctionSymbol.GetPublicSymbol().IsInitOnly);

var delegateSyntax = tree.GetRoot().DescendantNodes().OfType<DelegateDeclarationSyntax>().Single();
var delegateMemberSymbols = model.GetDeclaredSymbol(delegateSyntax).GetSymbol<SourceNamedTypeSymbol>().GetMembers();
Assert.True(delegateMemberSymbols.All(m => m is SourceDelegateMethodSymbol));
foreach (var member in delegateMemberSymbols)
{
if (member is MethodSymbol method)
{
Assert.False(method.IsInitOnly);
Assert.False(method.GetPublicSymbol().IsInitOnly);
}
}
}
}
}
Loading