diff --git a/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs b/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs index a495a91f19bbb..4c629b727670e 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ObsoleteAttributeHelpers.cs @@ -100,6 +100,7 @@ internal static ObsoleteDiagnosticKind GetObsoleteDiagnosticKind(Symbol symbol, case ObsoleteAttributeKind.None: return ObsoleteDiagnosticKind.NotObsolete; case ObsoleteAttributeKind.Experimental: + case ObsoleteAttributeKind.NewExperimental: return ObsoleteDiagnosticKind.Diagnostic; case ObsoleteAttributeKind.Uninitialized: // If we haven't cracked attributes on the symbol at all or we haven't @@ -162,6 +163,16 @@ static DiagnosticInfo createObsoleteDiagnostic(Symbol symbol, BinderFlags locati return new CSDiagnosticInfo(ErrorCode.WRN_Experimental, new FormattedSymbol(symbol, SymbolDisplayFormat.CSharpErrorMessageFormat)); } + if (data.Kind == ObsoleteAttributeKind.NewExperimental) + { + 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 var isColInit = location.Includes(BinderFlags.CollectionInitializerAddMethod); Debug.Assert(!isColInit || symbol.Name == WellKnownMemberNames.CollectionInitializerAddMethodName); diff --git a/src/Compilers/CSharp/Portable/Symbols/Symbol.cs b/src/Compilers/CSharp/Portable/Symbols/Symbol.cs index 17a55ad69b6d8..519d0471901c8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Symbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Symbol.cs @@ -1321,6 +1321,7 @@ internal ThreeState ObsoleteState { case ObsoleteAttributeKind.None: case ObsoleteAttributeKind.Experimental: + case ObsoleteAttributeKind.NewExperimental: return ThreeState.False; case ObsoleteAttributeKind.Uninitialized: return ThreeState.Unknown; diff --git a/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs b/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs index f8f464d31506e..599a60b520b25 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs @@ -166,6 +166,10 @@ internal static bool EarlyDecodeDeprecatedOrExperimentalOrObsoleteAttribute( { kind = ObsoleteAttributeKind.Experimental; } + else if (CSharpAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.NewExperimentalAttribute)) + { + kind = ObsoleteAttributeKind.NewExperimental; + } else { obsoleteData = null; 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..671250c5099a5 --- /dev/null +++ b/src/Compilers/CSharp/Test/Emit2/Semantics/ExperimentalAttributeTests.cs @@ -0,0 +1,846 @@ +// 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(); + } + + [Fact] + public void NullDiagnosticId() + { + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental(null)] +class C +{ + public static void M() { } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + 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 EmptyDiagnosticId() + { + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental(null)] +class C +{ + public static void M() { } +} +"""; + var comp = CreateCompilation(new[] { src, experimentalAttributeSrc }); + 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 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 still recognize 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); + } + + [Fact] + public void EmptyUrlFormat() + { + var src = """ +C.M(); + +[System.Diagnostics.CodeAnalysis.Experimental("DiagID1", UrlFormat = "")] +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("", 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 NullUrlFormat_Metadata() + { + var libSrc = """ +[System.Diagnostics.CodeAnalysis.Experimental("DiagID", UrlFormat = null)] +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 { 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) + ); + } + + [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(); + } +} diff --git a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs index 478a6c3f80911..96dce7cd00498 100644 --- a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs +++ b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs @@ -1163,18 +1163,82 @@ 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.ExperimentalAttribute); if (info.HasValue) { return TryExtractExperimentalDataFromAttribute(info); } + // [Experimental] is always a warning, not an error, so search for it last. + info = FindTargetAttribute(token, AttributeDescription.NewExperimentalAttribute); + if (info.HasValue) + { + return TryExtractNewExperimentalDataFromAttribute(info, decoder); + } + return null; } #nullable enable + private ObsoleteAttributeData? TryExtractNewExperimentalDataFromAttribute(AttributeInfo attributeInfo, IAttributeNamedArgumentDecoder decoder) + { + Debug.Assert(attributeInfo.HasValue); + if (!TryGetAttributeReader(attributeInfo.Handle, out var sig)) + { + return null; + } + + string? diagnosticId; + if (attributeInfo.SignatureIndex != 0) + { + throw ExceptionUtilities.UnexpectedValue(attributeInfo.SignatureIndex); + } + + // ExperimentalAttribute(string) + if (sig.RemainingBytes <= 0 || !CrackStringInAttributeValue(out diagnosticId, ref sig)) + { + return null; + } + + string? urlFormat = crackUrlFormat(decoder, ref sig); + return new ObsoleteAttributeData(ObsoleteAttributeKind.NewExperimental, 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); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index 59fe257f622ad..4ddbdf5a6d7da 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -465,6 +465,7 @@ static AttributeDescription() 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 NewExperimentalAttribute = 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..901a4d756869a 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/CommonAttributeData.cs @@ -257,9 +257,34 @@ internal ObsoleteAttributeData DecodeObsoleteAttribute(ObsoleteAttributeKind kin return DecodeDeprecatedAttribute(); case ObsoleteAttributeKind.Experimental: return DecodeExperimentalAttribute(); + case ObsoleteAttributeKind.NewExperimental: + return decodeNewExperimentalAttribute(); default: throw ExceptionUtilities.UnexpectedValue(kind); } + + ObsoleteAttributeData decodeNewExperimentalAttribute() + { + // ExperimentalAttribute(string diagnosticId) + Debug.Assert(this.CommonConstructorArguments.Length == 1); + string? diagnosticId = this.CommonConstructorArguments[0].ValueInternal as string; + + 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.NewExperimental, message: null, isError: false, diagnosticId, urlFormat); + } } /// diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs index bbc39d0322c65..9a7f2ca264963 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/ObsoleteAttributeData.cs @@ -13,6 +13,7 @@ internal enum ObsoleteAttributeKind Obsolete, Deprecated, Experimental, + NewExperimental, } /// diff --git a/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb b/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb index afeb698898f8c..b9efa63fc9602 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/ObsoleteAttributeHelpers.vb @@ -26,7 +26,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols Friend Shared Sub InitializeObsoleteDataFromMetadata(ByRef data As ObsoleteAttributeData, token As EntityHandle, containingModule As PEModuleSymbol) If data Is ObsoleteAttributeData.Uninitialized Then Dim obsoleteAttributeData As ObsoleteAttributeData = GetObsoleteDataFromMetadata(token, containingModule) - Interlocked.CompareExchange(data, obsoleteAttributeData, ObsoleteAttributeData.Uninitialized) + Interlocked.CompareExchange(data, obsoleteAttributeData, obsoleteAttributeData.Uninitialized) End If End Sub @@ -75,7 +75,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols Select Case symbol.ObsoleteKind Case ObsoleteAttributeKind.None Return ObsoleteDiagnosticKind.NotObsolete - Case ObsoleteAttributeKind.Experimental + Case ObsoleteAttributeKind.Experimental, ObsoleteAttributeKind.NewExperimental Return ObsoleteDiagnosticKind.Diagnostic Case ObsoleteAttributeKind.Uninitialized ' If we haven't cracked attributes on the symbol at all or we haven't @@ -122,6 +122,15 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols Return ErrorFactory.ErrorInfo(ERRID.WRN_Experimental, New FormattedSymbol(symbol, SymbolDisplayFormat.VisualBasicErrorMessageFormat)) End If + + If data.Kind = ObsoleteAttributeKind.NewExperimental 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/Source/QuickAttributeChecker.vb b/src/Compilers/VisualBasic/Portable/Symbols/Source/QuickAttributeChecker.vb index 7c5708de92f97..5c9eed809dc0b 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/Source/QuickAttributeChecker.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/Source/QuickAttributeChecker.vb @@ -157,6 +157,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols result = result Or QuickAttributes.Obsolete ElseIf Matches(name, inAttribute, AttributeDescription.ExperimentalAttribute) Then result = result Or QuickAttributes.Obsolete + ElseIf Matches(name, inAttribute, AttributeDescription.NewExperimentalAttribute) Then + result = result Or QuickAttributes.Obsolete ElseIf Matches(name, inAttribute, AttributeDescription.MyGroupCollectionAttribute) Then result = result Or QuickAttributes.TypeIdentifier ElseIf Matches(name, inAttribute, AttributeDescription.TypeIdentifierAttribute) Then diff --git a/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceModuleSymbol.vb b/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceModuleSymbol.vb index e208ec8ce966b..c3c9452d15149 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceModuleSymbol.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceModuleSymbol.vb @@ -329,6 +329,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols checker.AddName(AttributeDescription.ObsoleteAttribute.Name, QuickAttributes.Obsolete) checker.AddName(AttributeDescription.DeprecatedAttribute.Name, QuickAttributes.Obsolete) checker.AddName(AttributeDescription.ExperimentalAttribute.Name, QuickAttributes.Obsolete) + checker.AddName(AttributeDescription.NewExperimentalAttribute.Name, QuickAttributes.Obsolete) checker.AddName(AttributeDescription.MyGroupCollectionAttribute.Name, QuickAttributes.MyGroupCollection) checker.AddName(AttributeDescription.TypeIdentifierAttribute.Name, QuickAttributes.TypeIdentifier) diff --git a/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceNamedTypeSymbol.vb b/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceNamedTypeSymbol.vb index 76fa00c5be9f2..5a24eadf5c111 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceNamedTypeSymbol.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/Source/SourceNamedTypeSymbol.vb @@ -1818,6 +1818,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols AttributeDescription.ObsoleteAttribute.Name, AttributeDescription.DeprecatedAttribute.Name, AttributeDescription.ExperimentalAttribute.Name, + AttributeDescription.NewExperimentalAttribute.Name, AttributeDescription.MyGroupCollectionAttribute.Name, AttributeDescription.TypeIdentifierAttribute.Name diff --git a/src/Compilers/VisualBasic/Portable/Symbols/Symbol.vb b/src/Compilers/VisualBasic/Portable/Symbols/Symbol.vb index 60a69a52d1bcd..599465537a6e3 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.Experimental, ObsoleteAttributeKind.NewExperimental 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..82124d451b9bb 100644 --- a/src/Compilers/VisualBasic/Portable/Symbols/Symbol_Attributes.vb +++ b/src/Compilers/VisualBasic/Portable/Symbols/Symbol_Attributes.vb @@ -158,6 +158,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic kind = ObsoleteAttributeKind.Deprecated ElseIf VisualBasicAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.ExperimentalAttribute) Then kind = ObsoleteAttributeKind.Experimental + ElseIf VisualBasicAttributeData.IsTargetEarlyAttribute(type, syntax, AttributeDescription.NewExperimentalAttribute) Then + kind = ObsoleteAttributeKind.NewExperimental Else boundAttribute = Nothing obsoleteData = Nothing diff --git a/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests.vb b/src/Compilers/VisualBasic/Test/Emit/Attributes/AttributeTests.vb index 3616a6c63b0ff..9fa4e0e515609 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,127 @@ BC30045: Attribute constructor has a parameter of type 'Integer?', which is not ~~ ]]>) End Sub + + Private 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 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