diff --git a/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs b/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs index fd650d6f4a..ccf58ea289 100644 --- a/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs +++ b/src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs @@ -589,11 +589,33 @@ private static bool UsesOblivious(ISymbol symbol) private ApiName GetApiName(ISymbol symbol) { + var experimentName = getExperimentName(symbol); + return new ApiName( - getApiString(symbol, s_publicApiFormat), - getApiString(symbol, s_publicApiFormatWithNullability)); + getApiString(_compilation, symbol, experimentName, s_publicApiFormat), + getApiString(_compilation, symbol, experimentName, s_publicApiFormatWithNullability)); + + static string? getExperimentName(ISymbol symbol) + { + for (var current = symbol; current is not null; current = current.ContainingSymbol) + { + foreach (var attribute in current.GetAttributes()) + { + if (attribute.AttributeClass is { Name: "ExperimentalAttribute", ContainingSymbol: INamespaceSymbol { Name: nameof(System.Diagnostics.CodeAnalysis), ContainingNamespace: { Name: nameof(System.Diagnostics), ContainingNamespace: { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true } } } }) + { + if (attribute.ConstructorArguments is not [{ Kind: TypedConstantKind.Primitive, Type.SpecialType: SpecialType.System_String, Value: string diagnosticId }]) + return "???"; + + return diagnosticId; + + } + } + } + + return null; + } - string getApiString(ISymbol symbol, SymbolDisplayFormat format) + static string getApiString(Compilation compilation, ISymbol symbol, string? experimentName, SymbolDisplayFormat format) { string publicApiName = symbol.ToDisplayString(format); @@ -625,11 +647,16 @@ string getApiString(ISymbol symbol, SymbolDisplayFormat format) return string.Empty; } - if (symbol.ContainingAssembly != null && !symbol.ContainingAssembly.Equals(_compilation.Assembly)) + if (symbol.ContainingAssembly != null && !symbol.ContainingAssembly.Equals(compilation.Assembly)) { publicApiName += $" (forwarded, contained in {symbol.ContainingAssembly.Name})"; } + if (experimentName != null) + { + publicApiName = "[" + experimentName + "]" + publicApiName; + } + return publicApiName; } } diff --git a/src/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs b/src/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs index 3dba443976..13d62c956d 100644 --- a/src/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs +++ b/src/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs @@ -214,6 +214,11 @@ private async Task VerifyNet50CSharpAdditionalFileFixAsync(string source, string await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedApiText, oldUnshippedApiText, newUnshippedApiText, ReferenceAssemblies.Net.Net50); } + private async Task VerifyNet80CSharpAdditionalFileFixAsync(string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText) + { + await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedApiText, oldUnshippedApiText, newUnshippedApiText, AdditionalMetadataReferences.Net80); + } + private async Task VerifyAdditionalFileFixAsync(string language, string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText, ReferenceAssemblies? referenceAssemblies = null) { @@ -3219,6 +3224,77 @@ virtual R.PrintMembers(System.Text.StringBuilder! builder) -> bool await VerifyNet50CSharpAdditionalFileFixAsync(source, shippedText, unshippedText, fixedUnshippedText); } + [Fact] + [WorkItem(6759, "https://github.com/dotnet/roslyn-analyzers/issues/6759")] + public async Task TestExperimentalApiAsync() + { + var source = $$""" + using System.Diagnostics.CodeAnalysis; + + [Experimental("ID1")] + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + } + """; + + var shippedText = @""; + var unshippedText = @""; + var fixedUnshippedText = @"[ID1]C +[ID1]C.C() -> void"; + + await VerifyNet80CSharpAdditionalFileFixAsync(source, shippedText, unshippedText, fixedUnshippedText); + } + + [Theory] + [InlineData("")] + [InlineData("null")] + [InlineData("1")] + [InlineData("1, 2")] + [WorkItem(6759, "https://github.com/dotnet/roslyn-analyzers/issues/6759")] + public async Task TestExperimentalApiWithInvalidArgumentAsync(string invalidArgument) + { + var source = $$""" + using System.Diagnostics.CodeAnalysis; + + [Experimental({{invalidArgument}})] + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + } + """; + + var shippedText = @""; + var unshippedText = @""; + var fixedUnshippedText = @"[???]C +[???]C.C() -> void"; + + var test = new CSharpCodeFixTest() + { + ReferenceAssemblies = AdditionalMetadataReferences.Net80, + CompilerDiagnostics = CompilerDiagnostics.None, + TestState = + { + Sources = { source }, + AdditionalFiles = + { + (ShippedFileName, shippedText), + (UnshippedFileName, unshippedText), + }, + }, + FixedState = + { + AdditionalFiles = + { + (ShippedFileName, shippedText), + (UnshippedFileName, fixedUnshippedText), + }, + }, + }; + + test.DisabledDiagnostics.AddRange(DisabledDiagnostics); + + await test.RunAsync(); + } + #endregion } }