diff --git a/src/Compilers/CSharp/Portable/Binder/BinderFactory.BinderFactoryVisitor.cs b/src/Compilers/CSharp/Portable/Binder/BinderFactory.BinderFactoryVisitor.cs index 5a502ffc14748..84dd51c1cd3b0 100644 --- a/src/Compilers/CSharp/Portable/Binder/BinderFactory.BinderFactoryVisitor.cs +++ b/src/Compilers/CSharp/Portable/Binder/BinderFactory.BinderFactoryVisitor.cs @@ -290,7 +290,6 @@ public override Binder VisitAccessorDeclaration(AccessorDeclarationSyntax parent if ((object)propertySymbol != null) { accessor = (parent.Kind() == SyntaxKind.GetAccessorDeclaration) ? propertySymbol.GetMethod : propertySymbol.SetMethod; - // PROTOTYPE(partial-properties): check if a property with no accessors could fail this assertion, in which case we should either adjust the assertion or remove it. Debug.Assert(accessor is not null || parent.HasErrors); } break; diff --git a/src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.cs b/src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.cs index 56852488935b3..dcae3e0001579 100644 --- a/src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.cs +++ b/src/Compilers/CSharp/Portable/Compiler/DocumentationCommentCompiler.cs @@ -258,7 +258,14 @@ public override void DefaultVisit(Symbol symbol) bool shouldSkipPartialDefinitionComments = false; if (symbol.IsPartialDefinition()) { - if (symbol is MethodSymbol { PartialImplementationPart: MethodSymbol implementationPart }) + Symbol? implementationPart = symbol switch + { + MethodSymbol method => method.PartialImplementationPart, + SourcePropertySymbol property => property.PartialImplementationPart, + _ => null + }; + + if (implementationPart is not null) { Visit(implementationPart); diff --git a/src/Compilers/CSharp/Portable/Errors/MessageID.cs b/src/Compilers/CSharp/Portable/Errors/MessageID.cs index eb580e98e2ac6..958aa02a57bdd 100644 --- a/src/Compilers/CSharp/Portable/Errors/MessageID.cs +++ b/src/Compilers/CSharp/Portable/Errors/MessageID.cs @@ -284,6 +284,9 @@ internal enum MessageID IDS_FeatureParamsCollections = MessageBase + 12842, IDS_FeatureRefUnsafeInIteratorAsync = MessageBase + 12843, + + // PROTOTYPE(partial-properties): pack + IDS_FeaturePartialProperties = MessageBase + 13000, } // Message IDs may refer to strings that need to be localized. @@ -469,6 +472,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature) case MessageID.IDS_FeatureLockObject: case MessageID.IDS_FeatureParamsCollections: case MessageID.IDS_FeatureRefUnsafeInIteratorAsync: + case MessageID.IDS_FeaturePartialProperties: return LanguageVersion.Preview; // C# 12.0 features. diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs index 087dda527e4e1..5f79f0dae2f0a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs @@ -384,6 +384,18 @@ private static (DeclarationModifiers modifiers, bool hasExplicitAccessMod) MakeM hasExplicitAccessMod = true; } + if ((mods & DeclarationModifiers.Partial) != 0) + { + Debug.Assert(location.SourceTree is not null); + + LanguageVersion availableVersion = ((CSharpParseOptions)location.SourceTree.Options).LanguageVersion; + LanguageVersion requiredVersion = MessageID.IDS_FeaturePartialProperties.RequiredVersion(); + if (availableVersion < requiredVersion) + { + ModifierUtils.ReportUnsupportedModifiersForLanguageVersion(mods, DeclarationModifiers.Partial, location, diagnostics, availableVersion, requiredVersion); + } + } + ModifierUtils.CheckFeatureAvailabilityForStaticAbstractMembersInInterfacesIfNeeded(mods, isExplicitInterfaceImplementation, location, diagnostics); containingType.CheckUnsafeModifier(mods, location, diagnostics); @@ -631,7 +643,6 @@ private void PartialPropertyChecks(SourcePropertySymbol implementation, BindingD } if ((!hasTypeDifferences && !MemberSignatureComparer.PartialMethodsStrictComparer.Equals(this, implementation)) - // PROTOTYPE(partial-properties): test indexers with parameter name differences || !Parameters.SequenceEqual(implementation.Parameters, (a, b) => a.Name == b.Name)) { diagnostics.Add(ErrorCode.WRN_PartialPropertySignatureDifference, implementation.GetFirstLocation(), @@ -705,11 +716,13 @@ private void PartialPropertyChecks(SourcePropertySymbol implementation, BindingD private static BaseParameterListSyntax? GetParameterListSyntax(CSharpSyntaxNode syntax) => (syntax as IndexerDeclarationSyntax)?.ParameterList; + public sealed override bool IsExtern => PartialImplementationPart is { } implementation ? implementation.IsExtern : HasExternModifier; + internal SourcePropertySymbol? OtherPartOfPartial => _otherPartOfPartial; - internal bool IsPartialDefinition => IsPartial && !AccessorsHaveImplementation && !IsExtern; + internal bool IsPartialDefinition => IsPartial && !AccessorsHaveImplementation && !HasExternModifier; - internal bool IsPartialImplementation => IsPartial && (AccessorsHaveImplementation || IsExtern); + internal bool IsPartialImplementation => IsPartial && (AccessorsHaveImplementation || HasExternModifier); internal SourcePropertySymbol? PartialDefinitionPart => IsPartialImplementation ? OtherPartOfPartial : null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs index a003de34a608b..87c22aea218a8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs @@ -486,9 +486,20 @@ public override bool IsAbstract get { return (_modifiers & DeclarationModifiers.Abstract) != 0; } } + protected bool HasExternModifier + { + get + { + return (_modifiers & DeclarationModifiers.Extern) != 0; + } + } + public override bool IsExtern { - get { return (_modifiers & DeclarationModifiers.Extern) != 0; } + get + { + return HasExternModifier; + } } public override bool IsStatic diff --git a/src/Compilers/CSharp/Test/Emit2/Diagnostics/GetDiagnosticsTests.cs b/src/Compilers/CSharp/Test/Emit2/Diagnostics/GetDiagnosticsTests.cs index 4fcd0bf2fe66c..92cfc8eb21ea7 100644 --- a/src/Compilers/CSharp/Test/Emit2/Diagnostics/GetDiagnosticsTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Diagnostics/GetDiagnosticsTests.cs @@ -186,8 +186,6 @@ private void NonPartialMethod2() { } Assert.True(completedCompilationUnits.Contains(tree1.FilePath)); } - // PROTOTYPE(partial-properties): also test compilation events for complete and incomplete partial properties and their accessors - [Fact, WorkItem(7477, "https://github.com/dotnet/roslyn/issues/7477")] public void TestCompilationEventsForPartialMethod() { @@ -240,6 +238,69 @@ partial void PartialMethod() { } Assert.True(completedCompilationUnits.Contains(tree1.FilePath)); } + [Fact] + public void TestCompilationEventsForPartialProperty() + { + var source1 = @" +namespace N1 +{ + partial class Class + { + int NonPartialProp1 { get; set; } + partial int DefOnlyPartialProp { get; set; } + partial int ImplOnlyPartialProp { get => 1; set { } } + partial int PartialProp { get; set; } + } +} +"; + var source2 = @" +namespace N1 +{ + partial class Class + { + int NonPartialProp2 { get; set; } + partial int PartialProp { get => 1; set { } } + } +} +"; + + var tree1 = CSharpSyntaxTree.ParseText(source1, path: "file1"); + var tree2 = CSharpSyntaxTree.ParseText(source2, path: "file2"); + var eventQueue = new AsyncQueue(); + var compilation = CreateCompilationWithMscorlib45(new[] { tree1, tree2 }).WithEventQueue(eventQueue); + + // Invoke SemanticModel.GetDiagnostics to force populate the event queue for symbols in the first source file. + var model = compilation.GetSemanticModel(tree1); + model.GetDiagnostics(tree1.GetRoot().FullSpan); + + Assert.True(eventQueue.Count > 0); + bool compilationStartedFired; + HashSet declaredSymbolNames, completedCompilationUnits; + Assert.True(DequeueCompilationEvents(eventQueue, out compilationStartedFired, out declaredSymbolNames, out completedCompilationUnits)); + + // Verify symbol declared events fired for all symbols declared in the first source file. + Assert.True(compilationStartedFired); + + // NB: NonPartialProp2 is missing here because we only asked for diagnostics in tree1 + AssertEx.Equal([ + "", + "Class", + "DefOnlyPartialProp", + "get_ImplOnlyPartialProp", + "get_NonPartialProp1", + "get_PartialProp", + "ImplOnlyPartialProp", + "N1", + "NonPartialProp1", + "PartialProp", + "set_ImplOnlyPartialProp", + "set_NonPartialProp1", + "set_PartialProp" + ], declaredSymbolNames.OrderBy(name => name)); + + AssertEx.Equal(["file1"], completedCompilationUnits.OrderBy(name => name)); + } + [Fact, WorkItem(8178, "https://github.com/dotnet/roslyn/issues/8178")] public void TestEarlyCancellation() { @@ -288,12 +349,7 @@ private static bool DequeueCompilationEvents(AsyncQueue eventQ var added = declaredSymbolNames.Add(symbol.Name); if (!added) { - var method = symbol.GetSymbol() as Symbols.MethodSymbol; - Assert.NotNull(method); - - var isPartialMethod = method.PartialDefinitionPart != null || - method.PartialImplementationPart != null; - Assert.True(isPartialMethod, "Unexpected multiple symbol declared events for symbol " + symbol); + Assert.True(symbol.GetSymbol().IsPartialMember(), "Unexpected multiple symbol declared events for symbol " + symbol); } } else diff --git a/src/Compilers/CSharp/Test/Symbol/DocumentationComments/DocumentationCommentCompilerTests.cs b/src/Compilers/CSharp/Test/Symbol/DocumentationComments/DocumentationCommentCompilerTests.cs index c2b267aa23460..60a90b842ad31 100644 --- a/src/Compilers/CSharp/Test/Symbol/DocumentationComments/DocumentationCommentCompilerTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/DocumentationComments/DocumentationCommentCompilerTests.cs @@ -914,6 +914,7 @@ partial class C [Fact] public void PartialMethod_NoImplementation() { + // Whole document XML does not include the member, but single symbol XML does include it var source = @" partial class C { @@ -923,20 +924,25 @@ partial class C "; var tree = SyntaxFactory.ParseSyntaxTree(source, options: TestOptions.RegularWithDocumentationComments); - var comp = CreateCompilation(tree, assemblyName: "Test"); - var actual = GetDocumentationCommentText(comp); - var expected = @" - - - - Test - - - - -".Trim(); - Assert.Equal(expected, actual); + var method = comp.GlobalNamespace.GetMember("C.M"); + + AssertEx.AssertLinesEqual(expected: """ + + + + Test + + + + + """, actual: GetDocumentationCommentText(comp)); + + AssertEx.AssertLinesEqual(""" + + Summary 2 + + """, DocumentationCommentCompiler.GetDocumentationCommentXml(method, processIncludes: true, cancellationToken: default)); } [Fact] @@ -1490,6 +1496,690 @@ void verify(CSharpTestSource source) } } + /// Counterpart to . + [Fact] + public void PartialProperty_NoImplementation() + { + // Whole document XML does not include the member, but single symbol XML does include it + var source = @" +partial class C +{ + /** Summary 2*/ + public partial int P { get; set; } +} +"; + + var tree = SyntaxFactory.ParseSyntaxTree(source, options: TestOptions.RegularWithDocumentationComments); + var comp = CreateCompilation(tree, assemblyName: "Test"); + var property = comp.GlobalNamespace.GetMember("C.P"); + + AssertEx.AssertLinesEqual(expected: """ + + + + Test + + + + + """, actual: GetDocumentationCommentText(comp)); + + AssertEx.AssertLinesEqual(""" + + Summary 2 + + """, DocumentationCommentCompiler.GetDocumentationCommentXml(property, processIncludes: true, cancellationToken: default)); + } + + /// Counterpart to . + [Fact] + public void PartialProperties_MultipleFiles() + { + var source1 = @" +/// Summary 0 +public partial class C +{ + /** Summary 1*/ + public partial int P => 42; +} +"; + + var source2 = @" +public partial class C +{ + /** Summary 2*/ + public partial int P { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + var actualA = GetDocumentationCommentText(compA); + var expected = @" + + + + Test + + + + Summary 0 + + + Summary 1 + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + var actualB = GetDocumentationCommentText(compB); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialIndexers_MultipleFiles() + { + var source1 = @" +/// Summary 0 +public partial class C +{ + /** Summary 1*/ + public partial int this[int p] => 42; +} +"; + + var source2 = @" +public partial class C +{ + /** Summary 2*/ + public partial int this[int p] { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + var actualA = GetDocumentationCommentText(compA); + var expected = @" + + + + Test + + + + Summary 0 + + + Summary 1 + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + var actualB = GetDocumentationCommentText(compB); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialIndexer_NoImplementation() + { + // Whole document XML does not include the member, but single symbol XML does include it + var source = """ + partial class C + { + /// Summary 2 + /// My param + public partial int this[int p] { get; set; } + } + """; + + var tree = SyntaxFactory.ParseSyntaxTree(source, options: TestOptions.RegularWithDocumentationComments); + var comp = CreateCompilation(tree, assemblyName: "Test"); + var property = comp.GlobalNamespace.GetMember("C").Indexers.Single(); + + AssertEx.AssertLinesEqual(expected: """ + + + + Test + + + + + """, actual: GetDocumentationCommentText(comp)); + + AssertEx.AssertLinesEqual(""" + + Summary 2 + My param + + """, DocumentationCommentCompiler.GetDocumentationCommentXml(property, processIncludes: true, cancellationToken: default)); + } + + /// Counterpart to . + [Fact] + public void PartialProperties_MultipleFiles_DefinitionComment() + { + var source1 = @" +/// Summary 0 +public partial class C +{ + public partial int P => 42; +} +"; + + var source2 = @" +public partial class C +{ + /** Summary 2*/ + public partial int P { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + compA.VerifyDiagnostics(); + var actualA = GetDocumentationCommentText(compA); + var expected = @" + + + + Test + + + + Summary 0 + + + Summary 2 + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + compB.VerifyDiagnostics(); + var actualB = GetDocumentationCommentText(compB); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialProperties_MultipleFiles_ImplementationComment() + { + var source1 = @" +/// Summary 0 +public partial class C +{ + /** Summary 1*/ + public partial int P => 42; +} +"; + + var source2 = @" +public partial class C +{ + public partial int P { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + compA.VerifyDiagnostics(); + var actualA = GetDocumentationCommentText(compA); + var expected = @" + + + + Test + + + + Summary 0 + + + Summary 1 + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + compB.VerifyDiagnostics(); + var actualB = GetDocumentationCommentText(compB); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialProperties_MultipleFiles_NoComment() + { + var source1 = @" +/// Summary 0 +public partial class C +{ + public partial int P => 42; +} +"; + + var source2 = @" +public partial class C +{ + public partial int P { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + var expectedDiagnostics = new[] + { + // (4,24): warning CS1591: Missing XML comment for publicly visible type or member 'C.P' + // public partial int P { get; } + Diagnostic(ErrorCode.WRN_MissingXMLComment, "P").WithArguments("C.P").WithLocation(4, 24) + }; + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + compA.VerifyDiagnostics(expectedDiagnostics); + var actualA = GetDocumentationCommentText(compA, expectedDiagnostics); + var expected = @" + + + + Test + + + + Summary 0 + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + compB.VerifyDiagnostics(expectedDiagnostics); + var actualB = GetDocumentationCommentText(compB, expectedDiagnostics); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialProperties_MultipleFiles_Overlap() + { + var source1 = @" +partial class C +{ + /** Remarks 1 */ + public partial int P => 42; +} +"; + + var source2 = @" +partial class C +{ + /** Summary 2*/ + public partial int P { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + compA.VerifyDiagnostics(); + var actualA = GetDocumentationCommentText(compA); + var expected = @" + + + + Test + + + + Remarks 1 + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + compB.VerifyDiagnostics(); + var actualB = GetDocumentationCommentText(compB); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialProperties_MultipleFiles_ImplComment_Invalid() + { + var source1 = @" +partial class C +{ + /// + public partial int P => 42; +} +"; + + var source2 = @" +partial class C +{ + /** Summary 2*/ + public partial int P { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + var expectedDiagnostics = new[] + { + // (4,20): warning CS1570: XML comment has badly formed XML -- 'End tag 'a' does not match the start tag 'summary'.' + // /// + Diagnostic(ErrorCode.WRN_XMLParseError, "a").WithArguments("a", "summary").WithLocation(4, 20), + // (4,22): warning CS1570: XML comment has badly formed XML -- 'End tag was not expected at this location.' + // /// + Diagnostic(ErrorCode.WRN_XMLParseError, "<").WithLocation(4, 22) + }; + + // Files passed in order. + var compA = CreateCompilation(new[] { tree1, tree2 }, assemblyName: "Test"); + compA.VerifyDiagnostics(expectedDiagnostics); + var actualA = GetDocumentationCommentText(compA); + var expected = @" + + + + Test + + + + + +".Trim(); + AssertEx.Equal(expected, actualA); + + // Files passed in reverse order. + var compB = CreateCompilation(new[] { tree2, tree1 }, assemblyName: "Test"); + compB.VerifyDiagnostics(expectedDiagnostics); + var actualB = GetDocumentationCommentText(compB); + Assert.Equal(expected, actualB); + } + + /// Counterpart to . + [Fact] + public void PartialIndexer_Paramref_01() + { + var source1 = @" +partial class C +{ + /** Accepts . */ + public partial int this[int p1] => 42; +} +"; + + var source2 = @" +partial class C +{ + public partial int this[int p2] { get; } +} +"; + + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + verify(new[] { tree1, tree2 }); + + // Files passed in reverse order. + verify(new[] { tree2, tree1 }); + + void verify(CSharpTestSource source) + { + var compilation = CreateCompilation(source, assemblyName: "Test"); + var verifier = CompileAndVerify(compilation, symbolValidator: module => + { + var indexer = module.GlobalNamespace.GetMember("C").Indexers.Single(); + Assert.Equal("p2", indexer.Parameters.Single().Name); + }); + verifier.VerifyDiagnostics( + // (5,24): warning CS9308: Partial property declarations 'int C.this[int p2]' and 'int C.this[int p1]' have signature differences. + // public partial int this[int p1] => 42; + Diagnostic(ErrorCode.WRN_PartialPropertySignatureDifference, "this").WithArguments("int C.this[int p2]", "int C.this[int p1]").WithLocation(5, 24)); + + var actual = GetDocumentationCommentText(compilation); + var expected = """ + + + + Test + + + + Accepts . + + + + """.Trim(); + AssertEx.Equal(expected, actual); + } + } + + /// Counterpart to . + [Fact] + public void PartialIndexer_Paramref_02() + { + var source1 = @" +partial class C +{ + /** Accepts . */ + public partial int this[int p1] => 42; +} +"; + + var source2 = @" +partial class C +{ + public partial int this[int p2] { get; } +} +"; + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + verify(new[] { tree1, tree2 }); + + // Files passed in reverse order. + verify(new[] { tree2, tree1 }); + + void verify(CSharpTestSource source) + { + var compilation = CreateCompilation(source, assemblyName: "Test"); + var verifier = CompileAndVerify(compilation, symbolValidator: module => + { + var indexer = module.GlobalNamespace.GetMember("C").Indexers.Single(); + Assert.Equal("p2", indexer.Parameters.Single().Name); + }); + verifier.VerifyDiagnostics( + // (4,42): warning CS1734: XML comment on 'C.this[int]' has a paramref tag for 'p2', but there is no parameter by that name + // /** Accepts . */ + Diagnostic(ErrorCode.WRN_UnmatchedParamRefTag, "p2").WithArguments("p2", "C.this[int]").WithLocation(4, 42), + // (5,24): warning CS9308: Partial property declarations 'int C.this[int p2]' and 'int C.this[int p1]' have signature differences. + // public partial int this[int p1] => 42; + Diagnostic(ErrorCode.WRN_PartialPropertySignatureDifference, "this").WithArguments("int C.this[int p2]", "int C.this[int p1]").WithLocation(5, 24)); + + var actual = GetDocumentationCommentText(compilation, + // (4,42): warning CS1734: XML comment on 'C.this[int]' has a paramref tag for 'p2', but there is no parameter by that name + // /** Accepts . */ + Diagnostic(ErrorCode.WRN_UnmatchedParamRefTag, "p2").WithArguments("p2", "C.this[int]").WithLocation(4, 42)); + var expected = @" + + + + Test + + + + Accepts . + + + + ".Trim(); + AssertEx.Equal(expected, actual); + } + } + + /// Counterpart to . + [Fact] + public void PartialIndexer_Paramref_03() + { + var source1 = @" +partial class C +{ + public partial int this[int p1] => 42; +} +"; + + var source2 = @" +partial class C +{ + /** Accepts . */ + public partial int this[int p2] { get; } +} +"; + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + verify(new[] { tree1, tree2 }); + + // Files passed in reverse order. + verify(new[] { tree2, tree1 }); + + void verify(CSharpTestSource source) + { + var compilation = CreateCompilation(source, assemblyName: "Test"); + var verifier = CompileAndVerify(compilation, symbolValidator: module => + { + var indexer = module.GlobalNamespace.GetMember("C").Indexers.Single(); + Assert.Equal("p2", indexer.Parameters.Single().Name); + }); + verifier.VerifyDiagnostics( + // (4,24): warning CS9308: Partial property declarations 'int C.this[int p2]' and 'int C.this[int p1]' have signature differences. + // public partial int this[int p1] => 42; + Diagnostic(ErrorCode.WRN_PartialPropertySignatureDifference, "this").WithArguments("int C.this[int p2]", "int C.this[int p1]").WithLocation(4, 24), + // (4,42): warning CS1734: XML comment on 'C.this[int]' has a paramref tag for 'p1', but there is no parameter by that name + // /** Accepts . */ + Diagnostic(ErrorCode.WRN_UnmatchedParamRefTag, "p1").WithArguments("p1", "C.this[int]").WithLocation(4, 42)); + + var actual = GetDocumentationCommentText(compilation, + // (4,42): warning CS1734: XML comment on 'C.this[int]' has a paramref tag for 'p1', but there is no parameter by that name + // /** Accepts . */ + Diagnostic(ErrorCode.WRN_UnmatchedParamRefTag, "p1").WithArguments("p1", "C.this[int]").WithLocation(4, 42)); + var expected = @" + + + + Test + + + + Accepts . + + + +".Trim(); + AssertEx.Equal(expected, actual); + } + } + + /// Counterpart to . + [Fact] + public void PartialIndexer_Paramref_04() + { + var source1 = @" +partial class C +{ + public partial int this[int p1] => 42; +} +"; + + var source2 = @" +partial class C +{ + /** Accepts . */ + public partial int this[int p2] { get; } +} +"; + var tree1 = SyntaxFactory.ParseSyntaxTree(source1, options: TestOptions.RegularPreviewWithDocumentationComments); + var tree2 = SyntaxFactory.ParseSyntaxTree(source2, options: TestOptions.RegularPreviewWithDocumentationComments); + + // Files passed in order. + verify(new[] { tree1, tree2 }); + + // Files passed in reverse order. + verify(new[] { tree2, tree1 }); + + void verify(CSharpTestSource source) + { + var compilation = CreateCompilation(source, assemblyName: "Test"); + var verifier = CompileAndVerify(compilation, symbolValidator: module => + { + var indexer = module.GlobalNamespace.GetMember("C").Indexers.Single(); + Assert.Equal("p2", indexer.Parameters.Single().Name); + }); + verifier.VerifyDiagnostics( + // (4,24): warning CS9308: Partial property declarations 'int C.this[int p2]' and 'int C.this[int p1]' have signature differences. + // public partial int this[int p1] => 42; + Diagnostic(ErrorCode.WRN_PartialPropertySignatureDifference, "this").WithArguments("int C.this[int p2]", "int C.this[int p1]").WithLocation(4, 24)); + + var actual = GetDocumentationCommentText(compilation); + var expected = """ + + + + Test + + + + Accepts . + + + + """.Trim(); + AssertEx.Equal(expected, actual); + } + } + #endregion Partial methods #region Crefs diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs index 883640fca5b91..a92361f6fa327 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtendedPartialMethodsTests.cs @@ -1177,6 +1177,74 @@ static void validator(ModuleSymbol module) } } + [Fact] + public void Extern_Symbols_NoDllImport() + { + const string text1 = @" +public partial class C +{ + public static partial void M1(); + + public static extern partial void M1(); + + public static void M2() { M1(); } +}"; + + const string text2 = @" +public partial class C +{ + public static partial void M1(); + + public static extern partial void M1(); + + public static void M2() { M1(); } +}"; + const string expectedIL = @" +{ + // Code size 6 (0x6) + .maxstack 0 + IL_0000: call ""void C.M1()"" + IL_0005: ret +}"; + + var verifier = CompileAndVerify( + text1, + parseOptions: TestOptions.RegularWithExtendedPartialMethods, + sourceSymbolValidator: module => validator(module, isSource: true), + symbolValidator: module => validator(module, isSource: false), + // PEVerify fails when extern methods lack an implementation + verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + verifier.VerifyIL("C.M2", expectedIL); + + verifier = CompileAndVerify( + text2, + parseOptions: TestOptions.RegularWithExtendedPartialMethods, + sourceSymbolValidator: module => validator(module, isSource: true), + symbolValidator: module => validator(module, isSource: false), + // PEVerify fails when extern methods lack an implementation + verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + verifier.VerifyIL("C.M2", expectedIL); + + static void validator(ModuleSymbol module, bool isSource) + { + var type = module.ContainingAssembly.GetTypeByMetadataName("C"); + var method = type.GetMember("M1"); + + // 'IsExtern' is not round tripped when DllImport is missing + Assert.Equal(isSource, method.IsExtern); + if (method.PartialImplementationPart is MethodSymbol implementation) + { + Assert.True(method.IsPartialDefinition()); + Assert.Equal(isSource, implementation.IsExtern); + } + + var importData = method.GetDllImportData(); + Assert.Null(importData); + } + } + [Fact] public void Async_01() { diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs index e4d5d591b05a2..fa727c8878268 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/PartialPropertiesTests.cs @@ -5,9 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Reflection; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; @@ -637,23 +639,42 @@ partial class C [Fact] public void Extern_01() { - // PROTOTYPE(partial-properties): test that appropriate flags are set in metadata for the property accessors. - // See ExtendedPartialMethodsTests.Extern_Symbols as a starting point. var source = """ - partial class C + public partial class C { - partial int P { get; set; } - extern partial int P { get; set; } + public partial int P { get; set; } + public extern partial int P { get; set; } } """; - var comp = CreateCompilation([source, IsExternalInitTypeDefinition]); - comp.VerifyEmitDiagnostics( - ); - var prop = comp.GetMember("C.P"); - // PROTOTYPE(partial-properties): a partial method definition should delegate to its implementation part to implement this API, i.e. return 'true' here - Assert.False(prop.GetPublicSymbol().IsExtern); - Assert.True(prop.PartialImplementationPart!.GetPublicSymbol().IsExtern); + var verifier = CompileAndVerify( + [source, IsExternalInitTypeDefinition], + sourceSymbolValidator: verifySource, + symbolValidator: verifyMetadata, + // PEVerify fails when extern methods lack an implementation + verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + + void verifySource(ModuleSymbol module) + { + var prop = module.GlobalNamespace.GetMember("C.P"); + Assert.True(prop.GetPublicSymbol().IsExtern); + Assert.True(prop.GetMethod!.GetPublicSymbol().IsExtern); + Assert.True(prop.SetMethod!.GetPublicSymbol().IsExtern); + Assert.True(prop.PartialImplementationPart!.GetPublicSymbol().IsExtern); + Assert.True(prop.PartialImplementationPart!.GetMethod!.GetPublicSymbol().IsExtern); + Assert.True(prop.PartialImplementationPart!.SetMethod!.GetPublicSymbol().IsExtern); + } + + void verifyMetadata(ModuleSymbol module) + { + var prop = module.GlobalNamespace.GetMember("C.P"); + // IsExtern doesn't round trip from metadata when DllImportAttribute is missing + // This is consistent with the behavior for methods + Assert.False(prop.GetPublicSymbol().IsExtern); + Assert.False(prop.GetMethod!.GetPublicSymbol().IsExtern); + Assert.False(prop.SetMethod!.GetPublicSymbol().IsExtern); + } } [Fact] @@ -678,6 +699,134 @@ partial class C // extern partial int P { get; set; } Diagnostic(ErrorCode.ERR_DuplicateNameInClass, "P").WithArguments("C", "P").WithLocation(4, 24) ); + + var members = comp.GetMembers("C.P").SelectAsArray(m => (SourcePropertySymbol)m); + Assert.Equal(2, members.Length); + Assert.True(members[0].IsExtern); + Assert.True(members[0].IsPartialImplementation); + Assert.True(members[1].IsExtern); + Assert.True(members[1].IsPartialImplementation); + } + + /// Based on as a starting point. + [Fact] + public void Extern_03() + { + var source = """ + using System.Runtime.InteropServices; + + public partial class C + { + public static partial int P { get; set; } + public static extern partial int P + { + [DllImport("something.dll")] + get; + + [DllImport("something.dll")] + set; + } + } + """; + var verifier = CompileAndVerify( + [source, IsExternalInitTypeDefinition], + symbolValidator: module => verify(module, isSource: false), + sourceSymbolValidator: module => verify(module, isSource: true)); + verifier.VerifyDiagnostics(); + + void verify(ModuleSymbol module, bool isSource) + { + var prop = module.GlobalNamespace.GetMember("C.P"); + Assert.True(prop.GetPublicSymbol().IsExtern); + verifyAccessor(prop.GetMethod!); + verifyAccessor(prop.SetMethod!); + + if (isSource) + { + var implPart = ((SourcePropertySymbol)prop).PartialImplementationPart!; + Assert.True(implPart.GetPublicSymbol().IsExtern); + verifyAccessor(implPart.GetMethod!); + verifyAccessor(implPart.SetMethod!); + } + } + + void verifyAccessor(MethodSymbol accessor) + { + Assert.True(accessor.GetPublicSymbol().IsExtern); + + var importData = accessor.GetDllImportData()!; + Assert.NotNull(importData); + Assert.Equal("something.dll", importData.ModuleName); + Assert.Equal(accessor.MetadataName, importData.EntryPointName); // e.g. 'get_P' + Assert.Equal(CharSet.None, importData.CharacterSet); + Assert.False(importData.SetLastError); + Assert.False(importData.ExactSpelling); + Assert.Equal(MethodImplAttributes.PreserveSig, accessor.ImplementationAttributes); + Assert.Equal(CallingConvention.Winapi, importData.CallingConvention); + Assert.Null(importData.BestFitMapping); + Assert.Null(importData.ThrowOnUnmappableCharacter); + } + } + + /// Based on 'AttributeTests_WellKnownAttributes.TestPseudoAttributes_DllImport_OperatorsAndAccessors'. + [Theory] + [CombinatorialData] + public void TestPseudoAttributes_DllImport(bool attributesOnDefinition) + { + var source = $$""" +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +public partial class C +{ + public static partial int F + { + {{(attributesOnDefinition ? @"[DllImport(""a"")]" : "")}} get; + {{(attributesOnDefinition ? @"[DllImport(""b"")]" : "")}} set; + } + + public extern static partial int F + { + {{(attributesOnDefinition ? "" : @"[DllImport(""a"")]")}} get; + {{(attributesOnDefinition ? "" : @"[DllImport(""b"")]")}} set; + } +} +"""; + CompileAndVerify(source, parseOptions: TestOptions.RegularPreview.WithNoRefSafetyRulesAttribute(), assemblyValidator: (assembly) => + { + var metadataReader = assembly.GetMetadataReader(); + + // no backing fields should be generated -- all members are "extern" members: + Assert.Equal(0, metadataReader.FieldDefinitions.AsEnumerable().Count()); + + Assert.Equal(2, metadataReader.GetTableRowCount(TableIndex.ModuleRef)); + Assert.Equal(2, metadataReader.GetTableRowCount(TableIndex.ImplMap)); + var visitedEntryPoints = new Dictionary(); + + foreach (var method in metadataReader.GetImportedMethods()) + { + string moduleName = metadataReader.GetString(metadataReader.GetModuleReference(method.GetImport().Module).Name); + string entryPointName = metadataReader.GetString(method.Name); + switch (entryPointName) + { + case "get_F": + Assert.Equal("a", moduleName); + break; + + case "set_F": + Assert.Equal("b", moduleName); + break; + + default: + throw TestExceptionUtilities.UnexpectedValue(entryPointName); + } + + // This throws if we visit one entry point name twice. + visitedEntryPoints.Add(entryPointName, true); + } + + Assert.Equal(2, visitedEntryPoints.Count); + }); } [Fact] @@ -4185,7 +4334,122 @@ partial class C Diagnostic(ErrorCode.ERR_UnmanagedCallersOnlyRequiresStatic, "UnmanagedCallersOnly").WithLocation(17, 30)); } + [Fact] + public void IndexerParameterNameDifference() + { + var source = """ + using System; + + partial class C + { + public partial int this[int p1] { get; set; } + public partial int this[int p2] { get => p2; set { } } + + static void Main() + { + var c = new C(); + Console.Write(c[p1: 1]); + } + } + """; + + var verifier = CompileAndVerify(source, expectedOutput: "1"); + verifier.VerifyDiagnostics( + // (6,24): warning CS9308: Partial property declarations 'int C.this[int p1]' and 'int C.this[int p2]' have signature differences. + // public partial int this[int p2] { get => p2; set { } } + Diagnostic(ErrorCode.WRN_PartialPropertySignatureDifference, "this").WithArguments("int C.this[int p1]", "int C.this[int p2]").WithLocation(6, 24)); + + var indexer = ((CSharpCompilation)verifier.Compilation).GetMember("C").Indexers.Single(); + Assert.Equal("p1", indexer.Parameters.Single().Name); + } + + [Fact] + public void BindExpressionInPropertyWithoutAccessors() + { + // Exercise an assertion in 'BinderFactoryVisitor.VisitAccessorDeclaration'. + var source = """ + class C + { + int X = 1; + public int P + { + Console.Write(X); + } + } + """; + + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (3,9): warning CS0414: The field 'C.X' is assigned but its value is never used + // int X = 1; + Diagnostic(ErrorCode.WRN_UnreferencedFieldAssg, "X").WithArguments("C.X").WithLocation(3, 9), + // (4,16): error CS0548: 'C.P': property or indexer must have at least one accessor + // public int P + Diagnostic(ErrorCode.ERR_PropertyWithNoAccessors, "P").WithArguments("C.P").WithLocation(4, 16), + // (6,9): error CS1014: A get or set accessor expected + // Console.Write(X); + Diagnostic(ErrorCode.ERR_GetOrSetExpected, "Console").WithLocation(6, 9), + // (6,16): error CS1014: A get or set accessor expected + // Console.Write(X); + Diagnostic(ErrorCode.ERR_GetOrSetExpected, ".").WithLocation(6, 16), + // (6,17): error CS1014: A get or set accessor expected + // Console.Write(X); + Diagnostic(ErrorCode.ERR_GetOrSetExpected, "Write").WithLocation(6, 17), + // (6,22): error CS1513: } expected + // Console.Write(X); + Diagnostic(ErrorCode.ERR_RbraceExpected, "(").WithLocation(6, 22), + // (6,24): error CS8124: Tuple must contain at least two elements. + // Console.Write(X); + Diagnostic(ErrorCode.ERR_TupleTooFewElements, ")").WithLocation(6, 24), + // (6,25): error CS1519: Invalid token ';' in class, record, struct, or interface member declaration + // Console.Write(X); + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, ";").WithArguments(";").WithLocation(6, 25), + // (8,1): error CS1022: Type or namespace definition, or end-of-file expected + // } + Diagnostic(ErrorCode.ERR_EOFExpected, "}").WithLocation(8, 1)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var node = tree.GetRoot().DescendantNodes().OfType().Where(name => name.ToString() == "X").Last(); + Assert.Equal(SyntaxKind.TupleElement, node.Parent!.Kind()); + var symbolInfo = model.GetSymbolInfo(node); + Assert.Null(symbolInfo.Symbol); + Assert.Empty(symbolInfo.CandidateSymbols); + } + + [Fact] + public void LangVersion_01() + { + var source = """ + partial class C + { + public partial int P { get; set; } + public partial int P { get => 1; set { } } + + public partial int this[int i] { get; } + public partial int this[int i] { get => i; } + } + """; + + var comp = CreateCompilation(source, parseOptions: TestOptions.RegularNext); + comp.VerifyEmitDiagnostics(); + + comp = CreateCompilation(source, parseOptions: TestOptions.Regular12); + comp.VerifyEmitDiagnostics( + // (3,24): error CS8703: The modifier 'partial' is not valid for this item in C# 12.0. Please use language version 'preview' or greater. + // public partial int P { get; set; } + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "P").WithArguments("partial", "12.0", "preview").WithLocation(3, 24), + // (4,24): error CS8703: The modifier 'partial' is not valid for this item in C# 12.0. Please use language version 'preview' or greater. + // public partial int P { get => 1; set { } } + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "P").WithArguments("partial", "12.0", "preview").WithLocation(4, 24), + // (6,24): error CS8703: The modifier 'partial' is not valid for this item in C# 12.0. Please use language version 'preview' or greater. + // public partial int this[int i] { get; } + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "this").WithArguments("partial", "12.0", "preview").WithLocation(6, 24), + // (7,24): error CS8703: The modifier 'partial' is not valid for this item in C# 12.0. Please use language version 'preview' or greater. + // public partial int this[int i] { get => i; } + Diagnostic(ErrorCode.ERR_InvalidModifierForLanguageVersion, "this").WithArguments("partial", "12.0", "preview").WithLocation(7, 24)); + } + // PROTOTYPE(partial-properties): override partial property where base has modopt - // PROTOTYPE(partial-properties): test that doc comments work consistently with partial methods (and probably spec it as well) } } diff --git a/src/Compilers/Test/Core/Assert/AssertEx.cs b/src/Compilers/Test/Core/Assert/AssertEx.cs index 25f36959793e8..66444574226a3 100644 --- a/src/Compilers/Test/Core/Assert/AssertEx.cs +++ b/src/Compilers/Test/Core/Assert/AssertEx.cs @@ -838,11 +838,19 @@ private sealed class LineComparer : IEqualityComparer public int GetHashCode(string str) => str.Trim().GetHashCode(); } - public static void AssertLinesEqual(string expected, string actual, string message, string expectedValueSourcePath, int expectedValueSourceLine, bool escapeQuotes) - { - IEnumerable GetLines(string str) => + private static IEnumerable GetLines(string str) => str.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + public static void AssertLinesEqual(string expected, string actual) + { + AssertEx.Equal( + GetLines(expected), + GetLines(actual), + comparer: LineComparer.Instance); + } + + public static void AssertLinesEqual(string expected, string actual, string message, string expectedValueSourcePath, int expectedValueSourceLine, bool escapeQuotes) + { AssertEx.Equal( GetLines(expected), GetLines(actual), diff --git a/src/Compilers/Test/Utilities/CSharp/TestOptions.cs b/src/Compilers/Test/Utilities/CSharp/TestOptions.cs index fed48bba64878..5801478f74e88 100644 --- a/src/Compilers/Test/Utilities/CSharp/TestOptions.cs +++ b/src/Compilers/Test/Utilities/CSharp/TestOptions.cs @@ -42,6 +42,7 @@ public static class TestOptions public static readonly CSharpParseOptions Regular11 = Regular.WithLanguageVersion(LanguageVersion.CSharp11); public static readonly CSharpParseOptions Regular12 = Regular.WithLanguageVersion(LanguageVersion.CSharp12); public static readonly CSharpParseOptions RegularWithDocumentationComments = Regular.WithDocumentationMode(DocumentationMode.Diagnose); + public static readonly CSharpParseOptions RegularPreviewWithDocumentationComments = RegularPreview.WithDocumentationMode(DocumentationMode.Diagnose); public static readonly CSharpParseOptions RegularWithLegacyStrongName = Regular.WithFeature("UseLegacyStrongNameProvider"); public static readonly CSharpParseOptions WithoutImprovedOverloadCandidates = Regular.WithLanguageVersion(MessageID.IDS_FeatureImprovedOverloadCandidates.RequiredVersion() - 1); public static readonly CSharpParseOptions WithCovariantReturns = Regular.WithLanguageVersion(MessageID.IDS_FeatureCovariantReturnsForOverrides.RequiredVersion());