From cd51ca03f131e12e6c40388cad0138421f397568 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 5 Jul 2023 20:47:36 -0700 Subject: [PATCH] Add support for `System.Diagnostics.CodeAnalysis.ExperimentalAttribute` (#68702) --- .../Symbols/ObsoleteAttributeHelpers.cs | 16 +- .../CSharp/Portable/Symbols/Symbol.cs | 1 + .../Portable/Symbols/Symbol_Attributes.cs | 4 + .../Attributes/AttributeTests_Experimental.cs | 2 +- .../Semantics/ExperimentalAttributeTests.cs | 1000 +++++++++++++++++ .../Core/Portable/MetadataReader/PEModule.cs | 78 +- .../Attributes/AttributeDescription.cs | 3 +- .../Symbols/Attributes/CommonAttributeData.cs | 34 +- .../Attributes/ObsoleteAttributeData.cs | 3 +- .../Symbols/ObsoleteAttributeHelpers.vb | 12 +- .../VisualBasic/Portable/Symbols/Symbol.vb | 2 +- .../Portable/Symbols/Symbol_Attributes.vb | 2 + .../Test/Emit/Attributes/AttributeTests.vb | 219 +++- .../Attributes/AttributeTests_Experimental.vb | 2 +- 14 files changed, 1361 insertions(+), 17 deletions(-) create mode 100644 src/Compilers/CSharp/Test/Emit2/Semantics/ExperimentalAttributeTests.cs diff --git a/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs b/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs index a495a91f19bbb..f73759e6fa04f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs @@ -99,6 +99,7 @@ internal static ObsoleteDiagnosticKind GetObsoleteDiagnosticKind(Symbol symbol, { case ObsoleteAttributeKind.None: return ObsoleteDiagnosticKind.NotObsolete; + case ObsoleteAttributeKind.WindowsExperimental: case ObsoleteAttributeKind.Experimental: return ObsoleteDiagnosticKind.Diagnostic; case ObsoleteAttributeKind.Uninitialized: @@ -154,12 +155,23 @@ static DiagnosticInfo createObsoleteDiagnostic(Symbol symbol, BinderFlags locati return null; } - if (data.Kind == ObsoleteAttributeKind.Experimental) + if (data.Kind == ObsoleteAttributeKind.WindowsExperimental) { Debug.Assert(data.Message == null); Debug.Assert(!data.IsError); // Provide an explicit format for fully-qualified type names. - return new CSDiagnosticInfo(ErrorCode.WRN_Experimental, new FormattedSymbol(symbol, SymbolDisplayFormat.CSharpErrorMessageFormat)); + return new CSDiagnosticInfo(ErrorCode.WRN_Experimental, + new FormattedSymbol(symbol, SymbolDisplayFormat.CSharpErrorMessageFormat)); + } + + if (data.Kind == ObsoleteAttributeKind.Experimental) + { + Debug.Assert(data.Message is null); + Debug.Assert(!data.IsError); + + // Provide an explicit format for fully-qualified type names. + return new CustomObsoleteDiagnosticInfo(MessageProvider.Instance, (int)ErrorCode.WRN_Experimental, data, + new FormattedSymbol(symbol, SymbolDisplayFormat.CSharpErrorMessageFormat)); } // Issue a specialized diagnostic for add methods of collection initializers diff --git a/src/Compilers/CSharp/Portable/Symbols/Symbol.cs b/src/Compilers/CSharp/Portable/Symbols/Symbol.cs index 17a55ad69b6d8..0eff024ab0ed5 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Symbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Symbol.cs @@ -1320,6 +1320,7 @@ internal ThreeState ObsoleteState switch (ObsoleteKind) { case ObsoleteAttributeKind.None: + case ObsoleteAttributeKind.WindowsExperimental: case ObsoleteAttributeKind.Experimental: return ThreeState.False; case ObsoleteAttributeKind.Uninitialized: diff --git a/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs b/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs index f8f464d31506e..cede809659f7a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs @@ -162,6 +162,10 @@ internal static bool EarlyDecodeDeprecatedOrExperimentalOrObsoleteAttribute( { kind = ObsoleteAttributeKind.Deprecated; } + else if (CSharpAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.WindowsExperimentalAttribute)) + { + kind = ObsoleteAttributeKind.WindowsExperimental; + } else if (CSharpAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.ExperimentalAttribute)) { kind = ObsoleteAttributeKind.Experimental; diff --git a/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_Experimental.cs b/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_Experimental.cs index 43763951ad049..1fab435c09b03 100644 --- a/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_Experimental.cs +++ b/src/Compilers/CSharp/Test/Emit2/Attributes/AttributeTests_Experimental.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests { - public class AttributeTests_Experimental : CSharpTestBase + public class AttributeTests_WindowsExperimental : CSharpTestBase { private const string DeprecatedAttributeSource = @"using System; diff --git a/src/Compilers/CSharp/Test/Emit2/Semantics/ExperimentalAttributeTests.cs b/src/Compilers/CSharp/Test/Emit2/Semantics/ExperimentalAttributeTests.cs new file mode 100644 index 0000000000000..492ab58da3755 --- /dev/null +++ b/src/Compilers/CSharp/Test/Emit2/Semantics/ExperimentalAttributeTests.cs @@ -0,0 +1,1000 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable disable + +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests; + +// These tests cover the handling of System.Diagnostics.CodeAnalysis.ExperimentalAttribute. +public class ExperimentalAttributeTests : CSharpTestBase +{ + private const string experimentalAttributeSrc = """ +#nullable enable + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + + public string? UrlFormat { get; set; } + } +} +"""; + + private const string DefaultHelpLinkUri = "https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS8305)"; + + [Theory, CombinatorialData] + public void Simple(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID1", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void OnAssembly() + { + // Ignored on assemblies + var libSrc = """ +[assembly: System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + + var comp = CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + comp.VerifyDiagnostics(); + } + + [Fact] + public void OnModule() + { + // Ignored on modules + var libSrc = """ +[module: System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + + var comp = CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + comp.VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void OnStruct(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public struct S +{ + public static void M() { } +} +"""; + + var src = """ +S.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'S' is for evaluation purposes only and is subject to change or removal in future updates. + // S.M(); + Diagnostic("DiagID1", "S").WithArguments("S").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Theory, CombinatorialData] + public void OnEnum(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public enum E { } +"""; + + var src = """ +E e = default; +e.ToString(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // 0.cs(1,1): warning DiagID1: 'E' is for evaluation purposes only and is subject to change or removal in future updates. + // E e = default; + Diagnostic("DiagID1", "E").WithArguments("E").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Theory, CombinatorialData] + public void OnConstructor(bool inSource) + { + var libSrc = """ +public class C +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public C() { } +} +"""; + + var src = """ +_ = new C(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,5): warning DiagID1: 'C.C()' is for evaluation purposes only and is subject to change or removal in future updates. + // _ = new C(); + Diagnostic("DiagID1", "new C()").WithArguments("C.C()").WithLocation(1, 5) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Theory, CombinatorialData] + public void OnMethod(bool inSource) + { + var libSrc = """ +public class C +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'C.M()' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID1", "C.M()").WithArguments("C.M()").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Theory, CombinatorialData] + public void OnProperty(bool inSource) + { + var libSrc = """ +public class C +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public static int P => 0; +} +"""; + + var src = """ +_ = C.P; +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,5): warning DiagID1: 'C.P' is for evaluation purposes only and is subject to change or removal in future updates. + // _ = C.P; + Diagnostic("DiagID1", "C.P").WithArguments("C.P").WithLocation(1, 5) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Theory, CombinatorialData] + public void OnField(bool inSource) + { + var libSrc = """ +public class C +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public static int field = 0; +} +"""; + + var src = """ +_ = C.field; +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,5): warning DiagID1: 'C.field' is for evaluation purposes only and is subject to change or removal in future updates. + // _ = C.field; + Diagnostic("DiagID1", "C.field").WithArguments("C.field").WithLocation(1, 5) + ); + } + + [Theory, CombinatorialData] + public void OnEvent(bool inSource) + { + var libSrc = """ +public class C +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public static event System.Action Event; + + static void M() + { + Event(); + } +} +"""; + + var src = """ +C.Event += () => { }; +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'C.Event' is for evaluation purposes only and is subject to change or removal in future updates. + // C.Event += () => { }; + Diagnostic("DiagID1", "C.Event").WithArguments("C.Event").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void OnInterface(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public interface I +{ + void M(); +} +"""; + + var src = """ +I i = null; +i.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'I' is for evaluation purposes only and is subject to change or removal in future updates. + // I i = null; + Diagnostic("DiagID1", "I").WithArguments("I").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void OnDelegate(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public delegate void D(); +"""; + + var src = """ +D d = null; +d(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'D' is for evaluation purposes only and is subject to change or removal in future updates. + // D d = null; + Diagnostic("DiagID1", "D").WithArguments("D").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void OnParameter(bool inSource) + { + // Ignored on parameters + var libSrc = """ +public class C +{ + public static void M([System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] int i) { } +} +"""; + + var src = """ +C.M(42); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void OnReturnValue(bool inSource) + { + // Ignored on return value + var libSrc = """ +public class C +{ + [return: System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public static int M() => 0; +} +"""; + + var src = """ +_ = C.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void OnTypeParameter(bool inSource) + { + // Ignored on type parameters + var libSrc = """ +public class C<[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] T> { } +"""; + + var src = """ +C c = null; +c.ToString(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void NullDiagnosticId(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental(null)] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning CS8305: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic(ErrorCode.WRN_Experimental, "C").WithArguments("C").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void DiagnosticIdWithTrailingNewline(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("Diag\n")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning Diag + // : 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("Diag\n", "C").WithArguments("C").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void DiagnosticIdWithNewline(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("Diag\n01")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning Diag + // 01: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("Diag\n01", "C").WithArguments("C").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void WhitespaceDiagnosticId(bool inSource, + [CombinatorialValues("\"\"", "\" \"", "\"\\n\"")] string whitespace) + { + var libSrc = $$""" +[System.Diagnostics.CodeAnalysis.Experimental({{whitespace}})] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + + var comp = inSource + ? CreateCompilation(new CSharpTestSource[] { (src, "0.cs"), libSrc, experimentalAttributeSrc }) + : CreateCompilation((src, "0.cs"), references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // 0.cs(1,1): warning CS8305: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic(ErrorCode.WRN_Experimental, "C").WithArguments("C").WithLocation(1, 1) + ); + } + + [Fact] + public void SpacedDiagnosticId() + { + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental("Diag 01")] +class C +{ + public static void M() { } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + comp.VerifyDiagnostics( + // 0.cs(1,1): warning Diag 01: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("Diag 01", "C").WithArguments("C").WithLocation(1, 1) + ); + } + + [Fact] + public void BadAttribute_IntParameter() + { + // In source, if the attribute is improperly declared, but with the right number of parameters, we still recognize it + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental(42)] +class C +{ + public static void M() { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(int diagnosticId) + { + } + } +} +"""; + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (1,1): warning CS8305: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic(ErrorCode.WRN_Experimental, "C").WithArguments("C").WithLocation(1, 1) + ); + } + + [Fact] + public void BadAttribute_IntParameter_Metadata() + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental(42)] +public class C +{ + public static void M() { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(int diagnosticId) + { + } + } +} +"""; + + var libComp = CreateCompilation(libSrc); + + var src = """ +C.M(); +"""; + + var comp = CreateCompilation(src, references: new[] { libComp.EmitToImageReference() }); + comp.VerifyDiagnostics(); + } + + [Fact] + public void BadAttribute_TwoStringParameters() + { + // If the attribute is improperly declared, with a wrong number of parameters, we ignore it + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental("ignored", "ignored")] +class C +{ + public static void M() { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(string diagnosticId, string urlFormat) + { + } + } +} +"""; + var comp = CreateCompilation(src); + comp.VerifyDiagnostics(); + } + + [Fact] + public void BadAttribute_IntUrlFormatProperty_Metadata() + { + // A "UrlFormat" property with a type other than 'string' is ignored + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID", UrlFormat = 42)] +public class C +{ + public static void M() { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + public int UrlFormat { get; set; } + } +} +"""; + + var libComp = CreateCompilation(libSrc); + + var src = """ +C.M(); +"""; + + var comp = CreateCompilation(src, references: new[] { libComp.EmitToImageReference() }); + comp.VerifyDiagnostics( + // (1,1): warning DiagID: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void BadAttribute_UrlFormatField_Metadata() + { + // A field named "UrlFormat" is ignored + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID", UrlFormat = "hello")] +public class C +{ + public static void M() { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + public string UrlFormat = "hello"; + } +} +"""; + + var libComp = CreateCompilation(libSrc); + + var src = """ +C.M(); +"""; + + var comp = CreateCompilation(src, references: new[] { libComp.EmitToImageReference() }); + comp.VerifyDiagnostics( + // (1,1): warning DiagID: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void BadAttribute_OtherProperty_Metadata() + { + // A property that isn't named "UrlFormat" is ignored + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID", NotUrlFormat = "hello")] +public class C +{ + public static void M() { } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + public string NotUrlFormat { get; set; } + } +} +"""; + + var libComp = CreateCompilation(libSrc); + + var src = """ +C.M(); +"""; + + var comp = CreateCompilation(src, references: new[] { libComp.EmitToImageReference() }); + comp.VerifyDiagnostics( + // (1,1): warning DiagID: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void UrlFormat() + { + // Combine the DiagnosticId with the UrlFormat if present + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1", UrlFormat = "https://example.org/{0}")] +class C +{ + public static void M() { } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + comp.VerifyDiagnostics( + // 0.cs(1,1): warning DiagID1: 'C' is for evaluation purposes only and is subject to change or removal in future updates. (https://example.org/DiagID1) + // C.M(); + Diagnostic("DiagID1", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal("https://example.org/DiagID1", diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void BadUrlFormat() + { + // We use a default help URL if the UrlFormat is improper + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1", UrlFormat = "https://example.org/{0}{1}")] +class C +{ + public static void M() { } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + comp.VerifyDiagnostics( + // 0.cs(1,1): warning DiagID1: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID1", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Theory, CombinatorialData] + public void EmptyUrlFormat(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1", UrlFormat = "")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +C.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (1,1): warning DiagID1: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID1", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal("", diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void NullUrlFormat() + { + // We use a default help URL if the UrlFormat is improper + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1", UrlFormat = null)] +class C +{ + public static void M() { } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + comp.VerifyDiagnostics( + // 0.cs(1,1): warning DiagID1: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID1", "C").WithArguments("C").WithLocation(1, 1) + ); + + var diag = comp.GetDiagnostics().Single(); + Assert.Equal("DiagID1", diag.Id); + Assert.Equal(ErrorCode.WRN_Experimental, (ErrorCode)diag.Code); + Assert.Equal(DefaultHelpLinkUri, diag.Descriptor.HelpLinkUri); + } + + [Fact] + public void FullyQualified() + { + var src = """ +N.C.M(); + +namespace N +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + class C + { + public static void M() { } + } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + comp.VerifyDiagnostics( + // 0.cs(1,1): warning DiagID1: 'N.C' is for evaluation purposes only and is subject to change or removal in future updates. + // N.C.M(); + Diagnostic("DiagID1", "N.C").WithArguments("N.C").WithLocation(1, 1) + ); + } + + [Theory, CombinatorialData] + public void Suppressed(bool inSource) + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public class C +{ + public static void M() { } +} +"""; + + var src = """ +#pragma warning disable DiagID1 +C.M(); +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void InObsoleteMethod(bool inSource) + { + // Diagnostics for [Experimental] are not suppressed in [Obsolete] members + var libSrc = """ +public class C +{ + [System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] + public static void M() { } +} +"""; + + var src = """ +class D +{ + [System.Obsolete("obsolete", true)] + void M2() + { + C.M(); + } +} +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + comp.VerifyDiagnostics( + // (6,9): warning DiagID1: 'C.M()' is for evaluation purposes only and is subject to change or removal in future updates. + // C.M(); + Diagnostic("DiagID1", "C.M()").WithArguments("C.M()").WithLocation(6, 9) + ); + } + + [Theory, CombinatorialData] + public void WithObsolete(bool inSource) + { + var libSrc = """ +[System.Obsolete("error", true)] +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1")] +public class C +{ +} +"""; + + var src = """ +class D +{ + void M(C c) + { + } +} +"""; + + var comp = inSource + ? CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }) + : CreateCompilation(src, references: new[] { CreateCompilation(new[] { libSrc, experimentalAttributeSrc }).EmitToImageReference() }); + + if (inSource) + { + comp.VerifyDiagnostics( + // 0.cs(3,12): warning DiagID1: 'C' is for evaluation purposes only and is subject to change or removal in future updates. + // void M(C c) + Diagnostic("DiagID1", "C").WithArguments("C").WithLocation(3, 12) + ); + } + else + { + comp.VerifyDiagnostics( + // (3,12): error CS0619: 'C' is obsolete: 'error' + // void M(C c) + Diagnostic(ErrorCode.ERR_DeprecatedSymbolStr, "C").WithArguments("C", "error").WithLocation(3, 12) + ); + } + } +} diff --git a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs index eb4cef10960c6..b55cb860ba66c 100644 --- a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs +++ b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs @@ -1180,18 +1180,86 @@ internal ObsoleteAttributeData TryGetDeprecatedOrExperimentalOrObsoleteAttribute return obsoleteData; } - // [Experimental] is always a warning, not an - // error, so search for [Experimental] last. + // [Windows.Foundation.Metadata.Experimental] is always a warning, not an error. + info = FindTargetAttribute(token, AttributeDescription.WindowsExperimentalAttribute); + if (info.HasValue) + { + return TryExtractWindowsExperimentalDataFromAttribute(info); + } + + // [Experimental] is always a warning, not an error, so search for it last. info = FindTargetAttribute(token, AttributeDescription.ExperimentalAttribute); if (info.HasValue) { - return TryExtractExperimentalDataFromAttribute(info); + return TryExtractExperimentalDataFromAttribute(info, decoder); } return null; } #nullable enable + private ObsoleteAttributeData? TryExtractExperimentalDataFromAttribute(AttributeInfo attributeInfo, IAttributeNamedArgumentDecoder decoder) + { + Debug.Assert(attributeInfo.HasValue); + if (!TryGetAttributeReader(attributeInfo.Handle, out var sig)) + { + return null; + } + + if (attributeInfo.SignatureIndex != 0) + { + throw ExceptionUtilities.UnexpectedValue(attributeInfo.SignatureIndex); + } + + // ExperimentalAttribute(string) + if (sig.RemainingBytes <= 0 || !CrackStringInAttributeValue(out string? diagnosticId, ref sig)) + { + return null; + } + + if (string.IsNullOrWhiteSpace(diagnosticId)) + { + diagnosticId = null; + } + + string? urlFormat = crackUrlFormat(decoder, ref sig); + return new ObsoleteAttributeData(ObsoleteAttributeKind.Experimental, message: null, isError: false, diagnosticId, urlFormat); + + static string? crackUrlFormat(IAttributeNamedArgumentDecoder decoder, ref BlobReader sig) + { + if (sig.RemainingBytes <= 0) + { + return null; + } + + string? urlFormat = null; + + try + { + // See CIL spec section II.23.3 Custom attributes + // + // Next is a description of the optional “named” fields and properties. + // This starts with NumNamed– an unsigned int16 giving the number of “named” properties or fields that follow. + var numNamed = sig.ReadUInt16(); + for (int i = 0; i < numNamed && urlFormat is null; i++) + { + var ((name, value), isProperty, typeCode, /* elementTypeCode */ _) = decoder.DecodeCustomAttributeNamedArgumentOrThrow(ref sig); + if (typeCode == SerializationTypeCode.String && isProperty && value.ValueInternal is string stringValue) + { + if (urlFormat is null && name == ObsoleteAttributeData.UrlFormatPropertyName) + { + urlFormat = stringValue; + } + } + } + } + catch (BadImageFormatException) { } + catch (UnsupportedSignatureContent) { } + + return urlFormat; + } + } + internal string? GetFirstUnsupportedCompilerFeatureFromToken(EntityHandle token, IAttributeNamedArgumentDecoder attributeNamedArgumentDecoder, CompilerFeatureRequiredFeatures allowedFeatures) { List? infos = FindTargetAttributes(token, AttributeDescription.CompilerFeatureRequiredAttribute); @@ -1645,14 +1713,14 @@ private ObsoleteAttributeData TryExtractDeprecatedDataFromAttribute(AttributeInf } } - private ObsoleteAttributeData TryExtractExperimentalDataFromAttribute(AttributeInfo attributeInfo) + private ObsoleteAttributeData TryExtractWindowsExperimentalDataFromAttribute(AttributeInfo attributeInfo) { Debug.Assert(attributeInfo.HasValue); switch (attributeInfo.SignatureIndex) { case 0: // ExperimentalAttribute() - return ObsoleteAttributeData.Experimental; + return ObsoleteAttributeData.WindowsExperimental; default: throw ExceptionUtilities.UnexpectedValue(attributeInfo.SignatureIndex); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index d842b536c6a4c..cac5e72f1af3f 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -466,7 +466,8 @@ static AttributeDescription() internal static readonly AttributeDescription NullableAttribute = new AttributeDescription("System.Runtime.CompilerServices", "NullableAttribute", s_signaturesOfNullableAttribute); internal static readonly AttributeDescription NullableContextAttribute = new AttributeDescription("System.Runtime.CompilerServices", "NullableContextAttribute", s_signaturesOfNullableContextAttribute); internal static readonly AttributeDescription NullablePublicOnlyAttribute = new AttributeDescription("System.Runtime.CompilerServices", "NullablePublicOnlyAttribute", s_signatures_HasThis_Void_Boolean_Only); - internal static readonly AttributeDescription ExperimentalAttribute = new AttributeDescription("Windows.Foundation.Metadata", "ExperimentalAttribute", s_signatures_HasThis_Void_Only); + internal static readonly AttributeDescription WindowsExperimentalAttribute = new AttributeDescription("Windows.Foundation.Metadata", "ExperimentalAttribute", s_signatures_HasThis_Void_Only); + internal static readonly AttributeDescription ExperimentalAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "ExperimentalAttribute", s_signatures_HasThis_Void_String_Only); internal static readonly AttributeDescription ExcludeFromCodeCoverageAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "ExcludeFromCodeCoverageAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription EnumeratorCancellationAttribute = new AttributeDescription("System.Runtime.CompilerServices", "EnumeratorCancellationAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription SkipLocalsInitAttribute = new AttributeDescription("System.Runtime.CompilerServices", "SkipLocalsInitAttribute", s_signatures_HasThis_Void_Only); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeData.cs index 01913c92d4381..646706093ac29 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeData.cs @@ -255,6 +255,8 @@ internal ObsoleteAttributeData DecodeObsoleteAttribute(ObsoleteAttributeKind kin return DecodeObsoleteAttribute(); case ObsoleteAttributeKind.Deprecated: return DecodeDeprecatedAttribute(); + case ObsoleteAttributeKind.WindowsExperimental: + return DecodeWindowsExperimentalAttribute(); case ObsoleteAttributeKind.Experimental: return DecodeExperimentalAttribute(); default: @@ -262,6 +264,34 @@ internal ObsoleteAttributeData DecodeObsoleteAttribute(ObsoleteAttributeKind kin } } + private ObsoleteAttributeData DecodeExperimentalAttribute() + { + // ExperimentalAttribute(string diagnosticId) + Debug.Assert(this.CommonConstructorArguments.Length == 1); + string? diagnosticId = this.CommonConstructorArguments[0].ValueInternal as string; + + if (string.IsNullOrWhiteSpace(diagnosticId)) + { + diagnosticId = null; + } + + string? urlFormat = null; + foreach (var (name, value) in this.CommonNamedArguments) + { + if (urlFormat is null && name == ObsoleteAttributeData.UrlFormatPropertyName && IsStringProperty(ObsoleteAttributeData.UrlFormatPropertyName)) + { + urlFormat = value.ValueInternal as string; + } + + if (urlFormat is not null) + { + break; + } + } + + return new ObsoleteAttributeData(ObsoleteAttributeKind.Experimental, message: null, isError: false, diagnosticId, urlFormat); + } + /// /// Decode the arguments to ObsoleteAttribute. ObsoleteAttribute can have 0, 1 or 2 arguments. /// @@ -347,11 +377,11 @@ private ObsoleteAttributeData DecodeDeprecatedAttribute() /// /// Decode the arguments to ExperimentalAttribute. ExperimentalAttribute has 0 arguments. /// - private ObsoleteAttributeData DecodeExperimentalAttribute() + private ObsoleteAttributeData DecodeWindowsExperimentalAttribute() { // ExperimentalAttribute() Debug.Assert(this.CommonConstructorArguments.Length == 0); - return ObsoleteAttributeData.Experimental; + return ObsoleteAttributeData.WindowsExperimental; } internal static void DecodeMethodImplAttribute( diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs index bbc39d0322c65..67d7d4da7c831 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs @@ -12,6 +12,7 @@ internal enum ObsoleteAttributeKind Uninitialized, Obsolete, Deprecated, + WindowsExperimental, Experimental, } @@ -21,7 +22,7 @@ internal enum ObsoleteAttributeKind internal sealed class ObsoleteAttributeData { public static readonly ObsoleteAttributeData Uninitialized = new ObsoleteAttributeData(ObsoleteAttributeKind.Uninitialized, message: null, isError: false, diagnosticId: null, urlFormat: null); - public static readonly ObsoleteAttributeData Experimental = new ObsoleteAttributeData(ObsoleteAttributeKind.Experimental, message: null, isError: false, diagnosticId: null, urlFormat: null); + public static readonly ObsoleteAttributeData WindowsExperimental = new ObsoleteAttributeData(ObsoleteAttributeKind.WindowsExperimental, message: null, isError: false, diagnosticId: null, urlFormat: null); public const string DiagnosticIdPropertyName = "DiagnosticId"; public const string UrlFormatPropertyName = "UrlFormat"; diff --git a/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb b/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb index afeb698898f8c..e8367ab52beb4 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb @@ -75,7 +75,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols Select Case symbol.ObsoleteKind Case ObsoleteAttributeKind.None Return ObsoleteDiagnosticKind.NotObsolete - Case ObsoleteAttributeKind.Experimental + Case ObsoleteAttributeKind.WindowsExperimental, ObsoleteAttributeKind.Experimental Return ObsoleteDiagnosticKind.Diagnostic Case ObsoleteAttributeKind.Uninitialized ' If we haven't cracked attributes on the symbol at all or we haven't @@ -115,13 +115,21 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols ' uninitialized. Debug.Assert(Not data.IsUninitialized) - If data.Kind = ObsoleteAttributeKind.Experimental Then + If data.Kind = ObsoleteAttributeKind.WindowsExperimental Then Debug.Assert(data.Message Is Nothing) Debug.Assert(Not data.IsError) ' Provide an explicit format for fully-qualified type names. Return ErrorFactory.ErrorInfo(ERRID.WRN_Experimental, New FormattedSymbol(symbol, SymbolDisplayFormat.VisualBasicErrorMessageFormat)) End If + If data.Kind = ObsoleteAttributeKind.Experimental Then + Debug.Assert(data.Message Is Nothing) + Debug.Assert(Not data.IsError) + ' Provide an explicit format for fully-qualified type names. + Return New CustomObsoleteDiagnosticInfo(MessageProvider.Instance, ERRID.WRN_Experimental, + data, New FormattedSymbol(symbol, SymbolDisplayFormat.VisualBasicErrorMessageFormat)) + End If + ' For property accessors we report a special diagnostic which indicates whether the getter or setter is obsolete. ' For all other symbols, report the regular diagnostic. If symbol.IsAccessor() AndAlso (DirectCast(symbol, MethodSymbol).AssociatedSymbol).Kind = SymbolKind.Property Then diff --git a/src/Compilers/VisualBasic/Portable/Symbols/Symbol.vb b/src/Compilers/VisualBasic/Portable/Symbols/Symbol.vb index 60a69a52d1bcd..9771862391d5d 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/Symbol.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/Symbol.vb @@ -446,7 +446,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Friend ReadOnly Property ObsoleteState As ThreeState Get Select Case ObsoleteKind - Case ObsoleteAttributeKind.None, ObsoleteAttributeKind.Experimental + Case ObsoleteAttributeKind.None, ObsoleteAttributeKind.WindowsExperimental, ObsoleteAttributeKind.Experimental Return ThreeState.False Case ObsoleteAttributeKind.Uninitialized Return ThreeState.Unknown diff --git a/src/Compilers/VisualBasic/Portable/Symbols/Symbol_Attributes.vb b/src/Compilers/VisualBasic/Portable/Symbols/Symbol_Attributes.vb index 95a1f0c45f704..924870153f8cd 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/Symbol_Attributes.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/Symbol_Attributes.vb @@ -156,6 +156,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic kind = ObsoleteAttributeKind.Obsolete ElseIf VisualBasicAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.DeprecatedAttribute) Then kind = ObsoleteAttributeKind.Deprecated + ElseIf VisualBasicAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.WindowsExperimentalAttribute) Then + kind = ObsoleteAttributeKind.WindowsExperimental ElseIf VisualBasicAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.ExperimentalAttribute) Then kind = ObsoleteAttributeKind.Experimental Else diff --git a/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests.vb b/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests.vb index 3616a6c63b0ff..a41614fd8878e 100644 --- a/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests.vb +++ b/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests.vb @@ -6,7 +6,6 @@ Imports System.Collections.Immutable Imports System.IO Imports System.Reflection Imports System.Runtime.InteropServices -Imports System.Xml.Linq Imports Microsoft.CodeAnalysis.Emit Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.CodeAnalysis.VisualBasic @@ -4894,5 +4893,223 @@ BC30045: Attribute constructor has a parameter of type 'Integer?', which is not ~~ ]]>) End Sub + + Private Shared ReadOnly experimentalAttributeCSharpSrc As String = " +#nullable enable + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.All, Inherited = false)] + public sealed class ExperimentalAttribute : Attribute + { + public ExperimentalAttribute(string diagnosticId) { } + + public string? UrlFormat { get; set; } + } +} +" + + + Public Sub ExperimentalWithDiagnosticsId() + Dim attrComp = CreateCSharpCompilation(experimentalAttributeCSharpSrc) + + Dim src = + + +Class C +End Class + +Class D + Sub M(c As C) + End Sub +End Class +]]> + + + + Dim comp = CreateCompilation(src, references:={attrComp.EmitToImageReference()}) + + comp.AssertTheseDiagnostics( +) + + Dim diag = comp.GetDiagnostics().Single() + Assert.Equal("DiagID1", diag.Id) + Assert.Equal(ERRID.WRN_Experimental, diag.Code) + Assert.Equal("https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(BC42380)", diag.Descriptor.HelpLinkUri) + End Sub + + + Public Sub ExperimentalWithDiagnosticsId_FullyQualified() + Dim attrComp = CreateCSharpCompilation(experimentalAttributeCSharpSrc) + + Dim src = + + + Class C + End Class +End Namespace + +Class D + Sub M(c As N.C) + End Sub +End Class +]]> + + + + Dim comp = CreateCompilation(src, references:={attrComp.EmitToImageReference()}) + + comp.AssertTheseDiagnostics( +) + End Sub + + + Public Sub ExperimentalWithDiagnosticsId_WithObsolete() + Dim attrComp = CreateCSharpCompilation(experimentalAttributeCSharpSrc) + + Dim src = + + + +Class C +End Class + +Class D + Sub M(c As C) + End Sub +End Class +]]> + + + + Dim comp = CreateCompilation(src, references:={attrComp.EmitToImageReference()}) + + comp.AssertTheseDiagnostics( +) + End Sub + + + Public Sub ExperimentalWithDiagnosticsId_WithObsolete_Metadata() + Dim attrReference = CreateCSharpCompilation(experimentalAttributeCSharpSrc).EmitToImageReference() + + Dim libSrc = + + + +Public Class C +End Class +]]> + + + + Dim libComp = CreateCompilation(libSrc, references:={attrReference}) + + Dim src = " +Class D + Sub M(c As C) + End Sub +End Class +" + + Dim comp = CreateCompilation(src, references:={attrReference, libComp.EmitToImageReference()}) + + comp.AssertTheseDiagnostics( +) + + End Sub + + + Public Sub ExperimentalWithDiagnosticsIdAndUrlFormat() + Dim attrComp = CreateCSharpCompilation(experimentalAttributeCSharpSrc) + + Dim src = + + +Class C +End Class + +Class D + Sub M(c As C) + End Sub +End Class +]]> + + + + Dim comp = CreateCompilation(src, references:={attrComp.EmitToImageReference()}) + + comp.AssertTheseDiagnostics( +) + + Dim diag = Comp.GetDiagnostics().Single() + Assert.Equal("DiagID1", diag.Id) + Assert.Equal(ERRID.WRN_Experimental, diag.Code) + Assert.Equal("https://example.org/DiagID1", diag.Descriptor.HelpLinkUri) + End Sub + + + Public Sub ExperimentalWithDiagnosticsIdAndUrlFormat_InMetadata() + Dim attrReference = CreateCSharpCompilation(experimentalAttributeCSharpSrc).EmitToImageReference() + + Dim libSrc = + + +Public Class C +End Class +]]> + + + + Dim libComp = CreateCompilation(libSrc, references:={attrReference}) + + Dim src = " +Class D + Sub M(c As C) + End Sub +End Class +" + + Dim comp = CreateCompilation(src, references:={attrReference, libComp.EmitToImageReference()}) + + comp.AssertTheseDiagnostics( +) + + Dim diag = comp.GetDiagnostics().Single() + Assert.Equal("DiagID1", diag.Id) + Assert.Equal(ERRID.WRN_Experimental, diag.Code) + Assert.Equal("https://example.org/DiagID1", diag.Descriptor.HelpLinkUri) + End Sub + End Class End Namespace diff --git a/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests_Experimental.vb b/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests_Experimental.vb index a778be9564a78..af0f0b616f251 100644 --- a/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests_Experimental.vb +++ b/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests_Experimental.vb @@ -9,7 +9,7 @@ Imports Xunit Namespace Microsoft.CodeAnalysis.VisualBasic.UnitTests.Semantics - Public Class AttributeTests_Experimental + Public Class AttributeTests_WindowsExperimental Inherits BasicTestBase Private Shared ReadOnly DeprecatedAndExperimentalAttributeSource As XElement =