Skip to content
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

Merged
merged 11 commits into from
Aug 10, 2023

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Aug 1, 2023

Follow-up on #68702 based on feedback from runtime team.
This PR makes two changes:

  1. the [Experimental] attribute can be applied to assembly or module targets, and it will then apply to all contained types and members
  2. we don't warn when using an experimental type/member and we are within an experimental context

@jcouv jcouv self-assigned this Aug 1, 2023
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged Issues and PRs which have not yet been triaged by a lead label Aug 1, 2023
@jcouv jcouv force-pushed the exp-attr branch 2 times, most recently from 0812f9c to 8a1bc25 Compare August 2, 2023 17:51
@jcouv jcouv added this to the 17.8 milestone Aug 2, 2023
@jcouv jcouv removed the untriaged Issues and PRs which have not yet been triaged by a lead label Aug 2, 2023

static ObsoleteDiagnosticKind getExperimentalDiagnosticKind(Symbol containingMember, bool forceComplete)
{
switch (GetObsoleteContextState(containingMember, forceComplete, getStateFromSymbol: static (symbol) => symbol.ExperimentalState))
Copy link
Member Author

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);
Copy link
Member Author

@jcouv jcouv Aug 2, 2023

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)
Copy link
Member Author

@jcouv jcouv Aug 2, 2023

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

@jcouv jcouv marked this pull request as ready for review August 2, 2023 18:08
@jcouv jcouv requested a review from a team as a code owner August 2, 2023 18:08

#region ExperimentalAttribute
private ObsoleteAttributeData _obsoleteAttributeData = ObsoleteAttributeData.Uninitialized;
public ObsoleteAttributeData ObsoleteAttributeData
Copy link
Member

@cston cston Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObsoleteAttributeData

Should the property be named ExperimentalAttributeData if this is intended specifically for that attribute? #Closed


#region ExperimentalAttribute
private ObsoleteAttributeData _obsoleteAttributeData = ObsoleteAttributeData.Uninitialized;
public ObsoleteAttributeData ObsoleteAttributeData
Copy link
Member

@cston cston Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObsoleteAttributeData

ExperimentalAttributeData? #Closed

{
return namedTypeSymbol.Arity == 0
&& namedTypeSymbol.HasNameQualifier("System.Diagnostics.CodeAnalysis")
&& namedTypeSymbol.Name.Equals("ExperimentalAttribute", StringComparison.Ordinal);
Copy link
Member

@cston cston Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

&& namedTypeSymbol.Name.Equals("ExperimentalAttribute", StringComparison.Ordinal)

Consider checking Name before HasNameQualifier" since checking Name` may be cheaper and rule out mismatches more often. #Closed

static bool isExperimentalSymbol(NamedTypeSymbol namedTypeSymbol)
{
return namedTypeSymbol.Arity == 0
&& namedTypeSymbol.HasNameQualifier("System.Diagnostics.CodeAnalysis")
Copy link
Member

@cston cston Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namedTypeSymbol.HasNameQualifier("System.Diagnostics.CodeAnalysis")

Perhaps namedTypeSymbol.IsWellKnownDiagnosticsCodeAnalysisTopLevelType() since that method doesn't require splitting strings, and also checks the type is a top-level type in that namespace. #Closed


static ObsoleteDiagnosticKind getExperimentalDiagnosticKind(Symbol containingMember, bool forceComplete)
{
switch (GetObsoleteContextState(containingMember, forceComplete, getStateFromSymbol: static (symbol) => symbol.ExperimentalState))
Copy link
Member

@cston cston Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch

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();
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VerifyDiagnostics()

Should there be an error reported for C in C.M()? Are we not reporting errors with experimental APIs within an assembly with an assembly-level [Experimental] attribute? #Resolved

Copy link
Member Author

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()
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MissingAssemblyAndModule

Is this testing [Experimental] support? #Resolved

Copy link
Member Author

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);
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

""

Consider including a reference to C, such as _ = new C();. #Resolved

Copy link
Member

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);
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

""

Consider including a reference to C. #Resolved

@@ -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)
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetObsoleteContextState

Please consider sharing the code with GetExperimentalDiagnosticKind() below. #Resolved

Friend Overrides ReadOnly Property ObsoleteAttributeData As ObsoleteAttributeData
Get
Dim data = GetDecodedWellKnownAttributeData()
Return If(data IsNot Nothing, data.ExperimentalAttributeData, Nothing)
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If(data IsNot Nothing, data.ExperimentalAttributeData, Nothing)

data?.ExperimentalAttributeData #Resolved

Dim derivedComp = CreateCompilation(derivedSrc, references:={originalC.ToMetadataReference(), attrRef})
derivedComp.AssertNoDiagnostics()

Dim comp = CreateCompilation("", references:={derivedComp.ToMetadataReference(), retargetedC.ToMetadataReference(), attrRef})
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

""

Consider referencing C. #Resolved

}
}

var lazyNetModuleAttributesBag = _lazyNetModuleAttributesBag;
Copy link
Member

@cston cston Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_lazyNetModuleAttributesBag

Hmm, I'm surprised there is a _lazyNetModuleAttributesBag here rather than just delegating to the ModuleSymbol. That said, _lazyNetModuleAttributesBag wasn't added in this PR. #Resolved

Select Case symbol.ObsoleteKind
Case ObsoleteAttributeKind.None
If symbol.ContainingModule?.ObsoleteKind = ObsoleteAttributeKind.Uninitialized OrElse
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

symbol.ContainingModule?

We're allowing for ContainingModule and ContainingAssembly to be null here but not in C#. Is there a reason for the difference?

Copy link
Member Author

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.

@jcouv
Copy link
Member Author

jcouv commented Aug 8, 2023

@dotnet/roslyn-compiler for second review. Thanks

1 similar comment
@jcouv
Copy link
Member Author

jcouv commented Aug 8, 2023

@dotnet/roslyn-compiler for second review. Thanks

@RikkiGibson RikkiGibson self-assigned this Aug 8, 2023
Comment on lines +2866 to +2869
/// <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>
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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.)

Suggested change
/// <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;
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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

Copy link
Member Author

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
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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)
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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

Copy link
Member Author

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.
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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

Copy link
Member Author

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 has C.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))
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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

Copy link
Member Author

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.
Copy link
Member

@jjonescz jjonescz Aug 9, 2023

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?

/// <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; }
#Resolved

@jcouv jcouv requested a review from cston August 9, 2023 20:43
kind <> SymbolKind.Method AndAlso
kind <> SymbolKind.Event AndAlso
kind <> SymbolKind.Field AndAlso
kind <> SymbolKind.Property Then
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global.Microsoft.CodeAnalysis.

Are the namespace qualifiers needed?

@jcouv jcouv enabled auto-merge (squash) August 9, 2023 22:54
@jcouv jcouv merged commit 73689cc into dotnet:main Aug 10, 2023
25 checks passed
@ghost ghost modified the milestones: 17.8, Next Aug 10, 2023
@jcouv jcouv deleted the exp-attr branch August 10, 2023 05:40
@jcouv
Copy link
Member Author

jcouv commented Aug 10, 2023

FYI @tarekgh, this was merged (for VS 17.8p2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants