-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for assembly/module-level [Experimental]
and contextual suppression
#69316
Conversation
0812f9c
to
8a1bc25
Compare
|
||
static ObsoleteDiagnosticKind getExperimentalDiagnosticKind(Symbol containingMember, bool forceComplete) | ||
{ | ||
switch (GetObsoleteContextState(containingMember, forceComplete, getStateFromSymbol: static (symbol) => symbol.ExperimentalState)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 although I did not run into that situation, I'm re-using the existing mechanism that we use for delaying Obsolete diagnostics when some information isn't yet resolved.
{ | ||
return namedTypeSymbol.Arity == 0 | ||
&& namedTypeSymbol.HasNameQualifier("System.Diagnostics.CodeAnalysis") | ||
&& namedTypeSymbol.Name.Equals("ExperimentalAttribute", StringComparison.Ordinal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 This is a poor man's cycle avoidance solution. This would treat any type of the right shape as an ExperimentalAttribute
, not merely the well-known one... #Closed
Return namedType IsNot Nothing AndAlso | ||
namedType.Arity = 0 AndAlso | ||
namedType.HasNameQualifier("System.Diagnostics.CodeAnalysis", StringComparison.Ordinal) AndAlso | ||
namedType.Name.Equals("ExperimentalAttribute", StringComparison.Ordinal) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 I debated what kind of string comparison to use. On one hand, VB is more relaxed; on the other hand, this type is not defined by the VB language but by the BCL. I ended up opting for the stricter comparison, but feedback is welcome. #Closed
|
||
#region ExperimentalAttribute | ||
private ObsoleteAttributeData _obsoleteAttributeData = ObsoleteAttributeData.Uninitialized; | ||
public ObsoleteAttributeData ObsoleteAttributeData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
#region ExperimentalAttribute | ||
private ObsoleteAttributeData _obsoleteAttributeData = ObsoleteAttributeData.Uninitialized; | ||
public ObsoleteAttributeData ObsoleteAttributeData |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
return namedTypeSymbol.Arity == 0 | ||
&& namedTypeSymbol.HasNameQualifier("System.Diagnostics.CodeAnalysis") | ||
&& namedTypeSymbol.Name.Equals("ExperimentalAttribute", StringComparison.Ordinal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static bool isExperimentalSymbol(NamedTypeSymbol namedTypeSymbol) | ||
{ | ||
return namedTypeSymbol.Arity == 0 | ||
&& namedTypeSymbol.HasNameQualifier("System.Diagnostics.CodeAnalysis") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
static ObsoleteDiagnosticKind getExperimentalDiagnosticKind(Symbol containingMember, bool forceComplete) | ||
{ | ||
switch (GetObsoleteContextState(containingMember, forceComplete, getStateFromSymbol: static (symbol) => symbol.ExperimentalState)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the only difference between this switch
and the one above is the lambda. Consider refactoring to remove the local function and share the switch (GetObsoleteContextState(...)) { }
statement.
Func<Symbol, ThreeState> getStateFromSymbol;
switch (symbol.ObsoleteKind)
{
...
}
switch (GetObsoleteContextState(containingMember, forceComplete, getStateFromSymbol)
{
...
}
``` #Closed
@@ -77,18 +80,779 @@ public class C | |||
var src = """ | |||
C.M(); | |||
"""; | |||
var comp = CreateCompilation(new[] { src, libSrc, experimentalAttributeSrc }); | |||
comp.VerifyDiagnostics(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we not reporting errors with experimental APIs within an assembly with an assembly-level [Experimental] attribute?
Correct. That's the second part of the change (described in OP)
} | ||
|
||
[Fact] | ||
public void MissingAssemblyAndModule() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same answer as for VB
var derivedComp = CreateCompilation(derivedSrc, new[] { originalC.ToMetadataReference(), attrRef }, targetFramework: TargetFramework.Standard); | ||
derivedComp.VerifyDiagnostics(); | ||
|
||
var comp = CreateCompilation("", new[] { derivedComp.ToMetadataReference(), retargetedC.ToMetadataReference() }, targetFramework: TargetFramework.Standard); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I guess the reference should have been to Derived
to test we're recognizing the retargeting symbol as experimental.
var derivedComp = CreateCompilation(@base, new[] { originalC.ToMetadataReference(), attrRef }, targetFramework: TargetFramework.Standard); | ||
derivedComp.VerifyDiagnostics(); | ||
|
||
var comp = CreateCompilation("", new[] { derivedComp.ToMetadataReference(), retargetedC.ToMetadataReference() }, targetFramework: TargetFramework.Standard); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -85,7 +113,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols | |||
Return ObsoleteDiagnosticKind.Lazy | |||
End Select | |||
|
|||
Select Case GetObsoleteContextState(context, forceComplete) | |||
Select Case GetObsoleteContextState(context, forceComplete, getStateFromSymbol:=Function(s) s.ObsoleteState) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Friend Overrides ReadOnly Property ObsoleteAttributeData As ObsoleteAttributeData | ||
Get | ||
Dim data = GetDecodedWellKnownAttributeData() | ||
Return If(data IsNot Nothing, data.ExperimentalAttributeData, Nothing) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dim derivedComp = CreateCompilation(derivedSrc, references:={originalC.ToMetadataReference(), attrRef}) | ||
derivedComp.AssertNoDiagnostics() | ||
|
||
Dim comp = CreateCompilation("", references:={derivedComp.ToMetadataReference(), retargetedC.ToMetadataReference(), attrRef}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
} | ||
|
||
var lazyNetModuleAttributesBag = _lazyNetModuleAttributesBag; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Select Case symbol.ObsoleteKind | ||
Case ObsoleteAttributeKind.None | ||
If symbol.ContainingModule?.ObsoleteKind = ObsoleteAttributeKind.Uninitialized OrElse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an existing difference. Here's the relevant comment on the ContainingModule
and ContainingAssembly
APIs (although they weren't super helpful for me to understand...):
''' Returns the module [assembly] containing this symbol. If this symbol is shared
''' across multiple modules [assemblies], or doesn't belong to a module [assembly], returns Nothing.
I don't know about the sharing scenario. Maybe that's a problem for [Experimental] (seems like we'd still want to enforce). I'll dig a bit more.
@dotnet/roslyn-compiler for second review. Thanks |
1 similar comment
@dotnet/roslyn-compiler for second review. Thanks |
/// <summary> | ||
/// Returns data decoded from Obsolete attribute or null if there is no Obsolete attribute. | ||
/// This property returns ObsoleteAttributeData.Uninitialized if attribute arguments haven't been decoded yet. | ||
/// </summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this doc comment is duplicate of the base one, hence not needed.
Btw, should the base comment be updated to mention ExperimentalAttribute?
(Same for VB in multiple places.)
/// <summary> | |
/// Returns data decoded from Obsolete attribute or null if there is no Obsolete attribute. | |
/// This property returns ObsoleteAttributeData.Uninitialized if attribute arguments haven't been decoded yet. | |
/// </summary> | |
``` #Resolved |
|
||
if (GetAttributeDeclarations().IsEmpty) | ||
{ | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an optimization. It allows not producing a lazy diagnostic when we know from syntax that there are no attributes.
|
||
if (symbol is MethodSymbol { MethodKind: MethodKind.Constructor } method && isExperimentalSymbol(method.ContainingType)) | ||
{ | ||
// Skip for constructors of System.Diagnostics.CodeAnalysis.ExperimentalAttribute to mitigate cycles |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to skip for non-constructor methods? #Resolved
} | ||
|
||
if (symbol.ContainingModule.ObsoleteKind is ObsoleteAttributeKind.Experimental | ||
|| symbol.ContainingAssembly.ObsoleteKind is ObsoleteAttributeKind.Experimental) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we do this here? Couldn't we check containing member only when this symbol
's obsolete kind is None
? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, it's better to put there. That said, I don't think one should use both attributes (Obsolete and Experimental) :-P
Assert.Equal(ObsoleteAttributeKind.Experimental, comp.GetTypeByMetadataName("C").ContainingAssembly.ObsoleteKind); | ||
|
||
// Note: the assembly-level [Experimental] is equivalent to marking every type and member as experimental, | ||
// whereas a type-level [Experimental] is not equivalent to marking every nested type and member as experimental. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason for this? Why not mark only types experimental when assembly is marked experimental? Otherwise members "inherit" obsolete/experimental state from assembly but not from containing type which feels slightly weird. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hope to confirm this in LDM next week, although I don't think we should block the PR on that. Some people have voiced the same preference as you.
For me, the meaning of [Experimental]
on assembly is a shortcut that everything inside is marked as experimental.
The benefit of considering methods experimental too is that if we only mark types, then it's possible to use the methods without getting any warning:
- library1 is marked with
[assembly: Experimental]
and hasC.M()
- library2 returns an instance of
C
from an API (C CreateC()
) and suppresses the experimental warning - library3 does
var c = CreateC(); c.M(); // we'd like to warn here, I think
The whole purpose of [assembly: Experimental]
is that some contained symbols will inherit it from the assembly (otherwise the attribute has no effect). That is already a departure from how [Experimental]
normally works. I'm not sure it's less weird to only inherit to types :-P
@@ -95,13 +97,38 @@ private static ThreeState GetObsoleteContextState(Symbol symbol, bool forceCompl | |||
|
|||
internal static ObsoleteDiagnosticKind GetObsoleteDiagnosticKind(Symbol symbol, Symbol containingMember, bool forceComplete = false) | |||
{ | |||
if (symbol is NamedTypeSymbol namedTypeSymbol && isExperimentalSymbol(namedTypeSymbol)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this cycle avoidance for ExperimentalAttribute
but not for ObsoleteAttribute
? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for raising this. Turns out that now that ObsoleteAttributeData
is lazy on assembly/module symbols, we no longer need this extra layer of cycle protection. I'll remove
@@ -481,7 +481,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic | |||
End Property | |||
|
|||
''' <summary> | |||
''' Returns data decoded from Obsolete attribute or null if there is no Obsolete attribute. | |||
''' Returns data decoded from Obsolete/Experimental attribute or null if there is no Obsolete/Experimental attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you update Symbol.cs
similarly?
roslyn/src/Compilers/CSharp/Portable/Symbols/Symbol.cs
Lines 1343 to 1347 in 294cbb6
/// <summary> | |
/// Returns data decoded from <see cref="ObsoleteAttribute"/> attribute or null if there is no <see cref="ObsoleteAttribute"/> attribute. | |
/// This property returns <see cref="Microsoft.CodeAnalysis.ObsoleteAttributeData.Uninitialized"/> if attribute arguments haven't been decoded yet. | |
/// </summary> | |
internal abstract ObsoleteAttributeData ObsoleteAttributeData { get; } |
kind <> SymbolKind.Method AndAlso | ||
kind <> SymbolKind.Event AndAlso | ||
kind <> SymbolKind.Field AndAlso | ||
kind <> SymbolKind.Property Then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why check SymbolKind
here and not in C#?
Dim moduleObsoleteKind As ObsoleteAttributeKind? = symbol.ContainingModule?.ObsoleteKind | ||
Dim assemblyObsoleteKind As ObsoleteAttributeKind? = symbol.ContainingAssembly?.ObsoleteKind | ||
|
||
If moduleObsoleteKind = Global.Microsoft.CodeAnalysis.ObsoleteAttributeKind.Experimental OrElse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI @tarekgh, this was merged (for VS 17.8p2) |
Follow-up on #68702 based on feedback from runtime team.
This PR makes two changes:
[Experimental]
attribute can be applied to assembly or module targets, and it will then apply to all contained types and members