diff --git a/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs b/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs index 8c8619c8ba380..7e5db2c758191 100644 --- a/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs +++ b/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs @@ -594,10 +594,11 @@ private static bool UsesOblivious(ISymbol symbol) private ApiName GetApiName(ISymbol symbol) { var experimentName = getExperimentName(symbol); + var requiresUnsafe = getRequiresUnsafe(symbol); return new ApiName( - getApiString(_compilation, symbol, experimentName, s_publicApiFormat), - getApiString(_compilation, symbol, experimentName, s_publicApiFormatWithNullability)); + getApiString(_compilation, symbol, experimentName, requiresUnsafe, s_publicApiFormat), + getApiString(_compilation, symbol, experimentName, requiresUnsafe, s_publicApiFormatWithNullability)); static string? getExperimentName(ISymbol symbol) { @@ -625,7 +626,27 @@ private ApiName GetApiName(ISymbol symbol) return null; } - static string getApiString(Compilation compilation, ISymbol symbol, string? experimentName, SymbolDisplayFormat format) + static bool getRequiresUnsafe(ISymbol symbol) + { + foreach (var attribute in symbol.GetAttributes()) + { + // https://github.com/dotnet/roslyn/issues/82546: Confirm the attribute shape in BCL API review. + // https://github.com/dotnet/roslyn/issues/82791: Use the public Roslyn API when available. + if (attribute.AttributeClass is { Name: "RequiresUnsafeAttribute", ContainingSymbol: INamespaceSymbol { Name: "CompilerServices", ContainingNamespace: { Name: "Runtime", ContainingNamespace: { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true } } } }) + { + return true; + } + } + + if (symbol is IMethodSymbol { AssociatedSymbol: { } associatedSymbol }) + { + return getRequiresUnsafe(associatedSymbol); + } + + return false; + } + + static string getApiString(Compilation compilation, ISymbol symbol, string? experimentName, bool requiresUnsafe, SymbolDisplayFormat format) { string publicApiName = symbol.ToDisplayString(format); @@ -667,6 +688,11 @@ static string getApiString(Compilation compilation, ISymbol symbol, string? expe publicApiName = "[" + experimentName + "]" + publicApiName; } + if (requiresUnsafe) + { + publicApiName = "[RequiresUnsafe]" + publicApiName; + } + return publicApiName; } } diff --git a/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs b/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs index 02833097f4d57..8cd3b40dcd4cf 100644 --- a/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs +++ b/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs @@ -2941,6 +2941,155 @@ public async Task TestExperimentalApiWithInvalidArgumentAsync(string invalidArgu await test.RunAsync(); } + private const string RequiresUnsafeAttributeSource = """ + namespace System.Runtime.CompilerServices + { + internal sealed class RequiresUnsafeAttribute : Attribute { } + } + """; + + [Fact] + public Task TestRequiresUnsafeApiOnMethodAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [RequiresUnsafe] + {{EnabledModifierCSharp}} void {|{{AddNewApiId}}:M|}() { } + } + """, @"", @"", """ + C + C.C() -> void + [RequiresUnsafe]C.M() -> void + """); + + [Fact] + public Task TestRequiresUnsafeApiOnExternMethodAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + {{EnabledModifierCSharp}} extern void {|{{AddNewApiId}}:M1|}() { } + [RequiresUnsafe] + {{EnabledModifierCSharp}} extern void {|{{AddNewApiId}}:M2|}() { } + } + """, @"", @"", """ + C + C.C() -> void + extern C.M1() -> void + [RequiresUnsafe]extern C.M2() -> void + """); + + [Fact] + public Task TestRequiresUnsafeApiOnPropertyAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [RequiresUnsafe] + {{EnabledModifierCSharp}} int Property { {|{{AddNewApiId}}:get|}; {|{{AddNewApiId}}:set|}; } + } + """, @"", @"", """ + C + C.C() -> void + [RequiresUnsafe]C.Property.get -> int + [RequiresUnsafe]C.Property.set -> void + """); + + [Fact] + public Task TestRequiresUnsafeApiOnPropertyAccessorsAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + {{EnabledModifierCSharp}} int Property { [RequiresUnsafe] {|{{AddNewApiId}}:get|}; {|{{AddNewApiId}}:set|}; } + } + """, @"", @"", """ + C + C.C() -> void + C.Property.set -> void + [RequiresUnsafe]C.Property.get -> int + """); + + [Fact] + public Task TestRequiresUnsafeApiOnEventAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System; + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [RequiresUnsafe] + {{EnabledModifierCSharp}} event EventHandler {|{{AddNewApiId}}:MyEvent|}; + } + """, @"", @"", """ + C + C.C() -> void + [RequiresUnsafe]C.MyEvent -> System.EventHandler + """); + + private async Task VerifyRequiresUnsafeAdditionalFileFixAsync(string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText) + { + // The RequiresUnsafeAttribute is defined as internal in test source, so we need to provide + // internal API files and include the attribute type entries to satisfy the internal API analyzer. + // We put these entries in the Shipped file so they don't interfere with the Unshipped file diffs. + var internalApiForAttribute = """ + System.Runtime.CompilerServices.RequiresUnsafeAttribute + System.Runtime.CompilerServices.RequiresUnsafeAttribute.RequiresUnsafeAttribute() -> void + """; + + var test = new CSharpCodeFixTest() + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + CompilerDiagnostics = CompilerDiagnostics.None, + }; + + test.TestState.Sources.Add(source); + test.TestState.Sources.Add(RequiresUnsafeAttributeSource); + + if (IsInternalTest) + { + // For internal tests, ShippedFileName/UnshippedFileName are InternalAPI files. + // Put the RequiresUnsafeAttribute entries in the shipped file. + test.TestState.AdditionalFiles.Add((ShippedFileName, internalApiForAttribute)); + test.TestState.AdditionalFiles.Add((UnshippedFileName, oldUnshippedApiText ?? "")); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicShippedFileName, "")); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicUnshippedFileName, "")); + + test.FixedState.Sources.Add(source); + test.FixedState.Sources.Add(RequiresUnsafeAttributeSource); + test.FixedState.AdditionalFiles.Add((ShippedFileName, internalApiForAttribute)); + test.FixedState.AdditionalFiles.Add((UnshippedFileName, newUnshippedApiText)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicShippedFileName, "")); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicUnshippedFileName, "")); + } + else + { + // For public tests, provide internal API files with the attribute entries pre-populated. + if (shippedApiText != null) + test.TestState.AdditionalFiles.Add((ShippedFileName, shippedApiText)); + if (oldUnshippedApiText != null) + test.TestState.AdditionalFiles.Add((UnshippedFileName, oldUnshippedApiText)); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalShippedFileName, internalApiForAttribute)); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalUnshippedFileName, "")); + + test.FixedState.Sources.Add(source); + test.FixedState.Sources.Add(RequiresUnsafeAttributeSource); + test.FixedState.AdditionalFiles.Add((ShippedFileName, shippedApiText ?? string.Empty)); + test.FixedState.AdditionalFiles.Add((UnshippedFileName, newUnshippedApiText)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalShippedFileName, internalApiForAttribute)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalUnshippedFileName, "")); + } + + test.DisabledDiagnostics.AddRange(DisabledDiagnostics); + + await test.RunAsync(); + } + #endregion } }