Skip to content

Commit dab5d0e

Browse files
committed
Add support for ExperimentalAttribute
Fixes #6759
1 parent 8738efa commit dab5d0e

File tree

2 files changed

+107
-4
lines changed

2 files changed

+107
-4
lines changed

src/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -589,11 +589,33 @@ private static bool UsesOblivious(ISymbol symbol)
589589

590590
private ApiName GetApiName(ISymbol symbol)
591591
{
592+
var experimentName = getExperimentName(symbol);
593+
592594
return new ApiName(
593-
getApiString(symbol, s_publicApiFormat),
594-
getApiString(symbol, s_publicApiFormatWithNullability));
595+
getApiString(_compilation, symbol, experimentName, s_publicApiFormat),
596+
getApiString(_compilation, symbol, experimentName, s_publicApiFormatWithNullability));
597+
598+
static string? getExperimentName(ISymbol symbol)
599+
{
600+
for (var current = symbol; current is not null; current = current.ContainingSymbol)
601+
{
602+
foreach (var attribute in current.GetAttributes())
603+
{
604+
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 } } } })
605+
{
606+
if (attribute.ConstructorArguments is not [{ Kind: TypedConstantKind.Primitive, Type.SpecialType: SpecialType.System_String, Value: string diagnosticId }])
607+
return "???";
608+
609+
return diagnosticId;
610+
611+
}
612+
}
613+
}
614+
615+
return null;
616+
}
595617

596-
string getApiString(ISymbol symbol, SymbolDisplayFormat format)
618+
static string getApiString(Compilation compilation, ISymbol symbol, string? experimentName, SymbolDisplayFormat format)
597619
{
598620
string publicApiName = symbol.ToDisplayString(format);
599621

@@ -625,11 +647,16 @@ string getApiString(ISymbol symbol, SymbolDisplayFormat format)
625647
return string.Empty;
626648
}
627649

628-
if (symbol.ContainingAssembly != null && !symbol.ContainingAssembly.Equals(_compilation.Assembly))
650+
if (symbol.ContainingAssembly != null && !symbol.ContainingAssembly.Equals(compilation.Assembly))
629651
{
630652
publicApiName += $" (forwarded, contained in {symbol.ContainingAssembly.Name})";
631653
}
632654

655+
if (experimentName != null)
656+
{
657+
publicApiName = "[" + experimentName + "]" + publicApiName;
658+
}
659+
633660
return publicApiName;
634661
}
635662
}

src/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ private async Task VerifyNet50CSharpAdditionalFileFixAsync(string source, string
214214
await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedApiText, oldUnshippedApiText, newUnshippedApiText, ReferenceAssemblies.Net.Net50);
215215
}
216216

217+
private async Task VerifyNet80CSharpAdditionalFileFixAsync(string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText)
218+
{
219+
await VerifyAdditionalFileFixAsync(LanguageNames.CSharp, source, shippedApiText, oldUnshippedApiText, newUnshippedApiText, AdditionalMetadataReferences.Net80);
220+
}
221+
217222
private async Task VerifyAdditionalFileFixAsync(string language, string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText,
218223
ReferenceAssemblies? referenceAssemblies = null)
219224
{
@@ -3219,6 +3224,77 @@ virtual R.PrintMembers(System.Text.StringBuilder! builder) -> bool
32193224
await VerifyNet50CSharpAdditionalFileFixAsync(source, shippedText, unshippedText, fixedUnshippedText);
32203225
}
32213226

3227+
[Fact]
3228+
[WorkItem(6759, "https://github.com/dotnet/roslyn-analyzers/issues/6759")]
3229+
public async Task TestExperimentalApiAsync()
3230+
{
3231+
var source = $$"""
3232+
using System.Diagnostics.CodeAnalysis;
3233+
3234+
[Experimental("ID1")]
3235+
{{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|}
3236+
{
3237+
}
3238+
""";
3239+
3240+
var shippedText = @"";
3241+
var unshippedText = @"";
3242+
var fixedUnshippedText = @"[ID1]C
3243+
[ID1]C.C() -> void";
3244+
3245+
await VerifyNet80CSharpAdditionalFileFixAsync(source, shippedText, unshippedText, fixedUnshippedText);
3246+
}
3247+
3248+
[Theory]
3249+
[InlineData("")]
3250+
[InlineData("null")]
3251+
[InlineData("1")]
3252+
[InlineData("1, 2")]
3253+
[WorkItem(6759, "https://github.com/dotnet/roslyn-analyzers/issues/6759")]
3254+
public async Task TestExperimentalApiWithInvalidArgumentAsync(string invalidArgument)
3255+
{
3256+
var source = $$"""
3257+
using System.Diagnostics.CodeAnalysis;
3258+
3259+
[Experimental({{invalidArgument}})]
3260+
{{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|}
3261+
{
3262+
}
3263+
""";
3264+
3265+
var shippedText = @"";
3266+
var unshippedText = @"";
3267+
var fixedUnshippedText = @"[???]C
3268+
[???]C.C() -> void";
3269+
3270+
var test = new CSharpCodeFixTest<DeclarePublicApiAnalyzer, DeclarePublicApiFix, XUnitVerifier>()
3271+
{
3272+
ReferenceAssemblies = AdditionalMetadataReferences.Net80,
3273+
CompilerDiagnostics = CompilerDiagnostics.None,
3274+
TestState =
3275+
{
3276+
Sources = { source },
3277+
AdditionalFiles =
3278+
{
3279+
(ShippedFileName, shippedText),
3280+
(UnshippedFileName, unshippedText),
3281+
},
3282+
},
3283+
FixedState =
3284+
{
3285+
AdditionalFiles =
3286+
{
3287+
(ShippedFileName, shippedText),
3288+
(UnshippedFileName, fixedUnshippedText),
3289+
},
3290+
},
3291+
};
3292+
3293+
test.DisabledDiagnostics.AddRange(DisabledDiagnostics);
3294+
3295+
await test.RunAsync();
3296+
}
3297+
32223298
#endregion
32233299
}
32243300
}

0 commit comments

Comments
 (0)