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

Create attribute default arguments during binding #59750

Merged

Conversation

RikkiGibson
Copy link
Contributor

@RikkiGibson RikkiGibson commented Feb 24, 2022

Related to #47352

Before this change we had a dedicated code path for creating default arguments in attributes, separate from the path that creates default arguments everywhere else. This change consolidates us to the single path which creates BoundExpressions instead of TypedConstants.

#53618 becomes a lot simpler to implement and more similar to operation creation for calls after this change.

@RikkiGibson RikkiGibson force-pushed the boundattribute-defaultarguments branch 2 times, most recently from 1e40711 to 9ea3b85 Compare February 24, 2022 23:32
@RikkiGibson RikkiGibson force-pushed the boundattribute-defaultarguments branch from 9ea3b85 to 0af1c8f Compare February 24, 2022 23:43
@RikkiGibson RikkiGibson marked this pull request as ready for review February 25, 2022 00:46
@RikkiGibson RikkiGibson requested a review from a team as a code owner February 25, 2022 00:46
afterAttributePartBound?.Invoke(node);
return (GetAttribute(boundAttribute, diagnostics), boundAttribute);
}

internal BoundAttribute BindAttribute(AttributeSyntax node, NamedTypeSymbol attributeType, BindingDiagnosticBag diagnostics)
internal BoundAttribute BindAttribute(AttributeSyntax node, NamedTypeSymbol attributeType, BindingDiagnosticBag diagnostics, Symbol? attributedMember = null)
Copy link
Member

@Youssef1313 Youssef1313 Feb 25, 2022

Choose a reason for hiding this comment

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

Symbol? attributedMember = null

Are we sure that callers not passing this parameter work properly? For example CSharpSemanticModel.GetSpeculativeSymbolInfo(int position, AttributeSyntax attribute) comes across this code path. This parameter as far as I understand affects CallerMemberName which I think there is no speculative semantic model tests for.

There is a second caller in AttributeSemanticModel that needs to be revised, I think.


e.g:

            var tree = SyntaxFactory.ParseSyntaxTree(@"
using System;
using System.Runtime.CompilerServices;

public class MyAttribute : Attribute
{
    public MyAttribute([CallerMemberName] string caller = null)
    {
    }
}

[Obsolete]
public class C
{
}
");
            // Writing in a clean console app so CreateCompilation helper of unit tests not available.
            var comp = CSharpCompilation.Create("", new[] { tree }, new[] { MetadataReference.CreateFromFile(typeof(CallerMemberNameAttribute).Assembly.Location) });
            var model = comp.GetSemanticModel(tree);
            var newAttributeSyntax = SyntaxFactory.ParseCompilationUnit("[My]class C { }").DescendantNodes().OfType<AttributeSyntax>().Single();
            var speculativeInfo = model.GetSpeculativeSymbolInfo(tree.GetRoot().DescendantNodes().First(n => n.ToString() == "[Obsolete]").SpanStart, newAttributeSyntax);
            Console.WriteLine(speculativeInfo.Symbol.ToDisplayString()); // prints MyAttribute.MyAttribute(string)

More extensive tests may be needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to come up with a speculative test which observes the value of implicit default arguments before this change. After this change, we pass the following test:

        [Fact]
        public void TestAttributeCallerInfoSemanticModel_Speculative()
        {
            var source = @"
using System;
using System.Runtime.CompilerServices;

class Attr : Attribute { public Attr([CallerMemberName] string s = null) { } }

class C
{
    [Attr(""a"")]
    void M0() { }
}
";
            var comp = CreateCompilation(source);
            comp.VerifyDiagnostics();

            var tree = comp.SyntaxTrees[0];
            var root = tree.GetRoot();
            var attrSyntax = root.DescendantNodes().OfType<AttributeSyntax>().Last();

            var semanticModel = comp.GetSemanticModel(tree);

            var newRoot = root.ReplaceNode(attrSyntax, attrSyntax.WithArgumentList(SyntaxFactory.ParseAttributeArgumentList("()")));
            var newAttrSyntax = newRoot.DescendantNodes().OfType<AttributeSyntax>().Last();

            Assert.True(semanticModel.TryGetSpeculativeSemanticModel(attrSyntax.ArgumentList.Position, newAttrSyntax, out var speculativeModel));

            var operation = speculativeModel.GetOperation(newAttrSyntax);
            VerifyOperationTree(comp, operation, @"
IOperation:  (OperationKind.None, Type: Attr) (Syntax: 'Attr()')
  Children(1):
      IDefaultValueOperation (OperationKind.DefaultValue, Type: System.String, Constant: null, IsImplicit) (Syntax: 'Attr()')");
        }

Before this PR, the 'None' operation has no children.
In other words, this implementation doesn't propagate the caller info properly. To do that, we would actually need to determine the attributedMember. Perhaps the implementation of IAttributeOperation would be a reasonable time to solve that.

Copy link
Member

Choose a reason for hiding this comment

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

Getting IOperartion from speculative semantic model sounds interesting!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FWIW, I think it could be difficult to do properly. I don't know how the rest of the team feels about it but it's the kind of thing I think would be reasonable to file as a follow-up bug after initial implementation of IAttributeOperation. It seems like the use case for CallerInfo is at runtime. If you are writing a tool that uses IOperation, etc. there's no reason to dig into the default arguments to get member name, line number, etc. values, just use the compiler APIs directly at that point.

@@ -121,14 +121,14 @@ internal partial class Binder
BindingDiagnosticBag diagnostics)
{
beforeAttributePartBound?.Invoke(node);
var boundAttribute = new ExecutableCodeBinder(node, this.ContainingMemberOrLambda, this).BindAttribute(node, boundAttributeType, diagnostics);
var boundAttribute = new ExecutableCodeBinder(node, this.ContainingMemberOrLambda, this).BindAttribute(node, boundAttributeType, (this as ContextualAttributeBinder)?.AttributedMember, diagnostics);
Copy link
Member

Choose a reason for hiding this comment

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

(this as ContextualAttributeBinder)

Are there valid cases where the ContextualAttributeBinder is in the Next chain rather than this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, if a ContextualAttributeBinder is present, it will be at the top of the chain here. I did actually consider eliminating the type entirely but I was concerned it could result in too much additional churn.

Copy link
Member

Choose a reason for hiding this comment

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

if a ContextualAttributeBinder is present, it will be at the top of the chain here

Consider asserting that.

Debug.Assert(this is ContextualAttributeBinder || !GetAllBinders().Any(this is ContextualAttributeBinder));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to write a reasonable assertion in a scenario like the following.

[Fact]
public void LambdaAttributes_06()
{
var source =
@"using System;
class AAttribute : Attribute
{
public AAttribute(Action a) { }
}
[A([B] () => { })]
class BAttribute : Attribute
{
}";
var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview);
comp.VerifyDiagnostics(
// (6,2): error CS0181: Attribute constructor parameter 'a' has type 'Action', which is not a valid attribute parameter type
// [A([B] () => { })]
Diagnostic(ErrorCode.ERR_BadAttributeParamType, "A").WithArguments("a", "System.Action").WithLocation(6, 2));
}

In this case, the BoundAttribute for [B] doesn't have errors, so there's not something conveniently available in scope for us to grab onto that lets us skip asserting that "ContextualAttributeBinder is only found on the top".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, I did try eliminating ContextualAttributeBinder, but I found we still depend on it in Binder.GetForwardedToAssembly to break cycles.

if (hasErrors || attributeConstructor.ParameterCount == 0)
{
constructorArgumentsSourceIndices = default(ImmutableArray<int>);
constructorArguments = constructorArgsArray;
rewrittenArguments = constructorArgsArray;
Copy link
Member

@cston cston Feb 28, 2022

Choose a reason for hiding this comment

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

rewrittenArguments = constructorArgsArray

Is it possible to get here with an attribute constructor with params argument and where the length of rewrittenArguments does not match the parameter length (say sharplab.io)? Do consumers of the SourceAttributeData expect the lengths to match?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll write a test against the compiler prior to this PR and ensure we haven't changed the behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the test AttributeTests.TestBadParamsCtor to answer this. It looks like the arguments and parameters length are only required to match when the attribute doesn't have errors.

argumentIndex > -1 && argumentIndex < argumentsCount)
else if (callerSourceLocation is object
&& !parameter.IsCallerMemberName
&& Conversions.ClassifyBuiltInConversion(Compilation.GetSpecialType(SpecialType.System_String), parameterType, ref discardedUseSiteInfo).Exists
Copy link
Member

Choose a reason for hiding this comment

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

&& Conversions.ClassifyBuiltInConversion(Compilation.GetSpecialType(SpecialType.System_String), parameterType, ref discardedUseSiteInfo).Exists

Are we testing this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

IIRC, I added this bit to satisfy some existing CallerArgumentExpression tests--the attribute is not expected to work when the attributed parameter only has a user-defined conversion from string.

@@ -1378,13 +1395,16 @@ BoundExpression bindDefaultArgument(SyntaxNode syntax, ParameterSymbol parameter
string path = callerSourceLocation.SourceTree.GetDisplayPath(callerSourceLocation.SourceSpan, Compilation.Options.SourceReferenceResolver);
defaultValue = new BoundLiteral(syntax, ConstantValue.Create(path), Compilation.GetSpecialType(SpecialType.System_String)) { WasCompilerGenerated = true };
}
else if (callerSourceLocation is object && parameter.IsCallerMemberName)
else if (callerSourceLocation is object && parameter.IsCallerMemberName && containingMember is not null)
Copy link
Member

Choose a reason for hiding this comment

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

&& containingMember is not null

Are we silently ignoring [CallerMemberName] for attributes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are not ignoring them in batch compilation scenarios, but we are in semantic model (IOperation) scenarios. See #59750 (comment)

@RikkiGibson
Copy link
Contributor Author

@cston Thank you for the review. I believe I've addressed all your feedback.

MethodSymbol attributeConstructor,
ImmutableArray<TypedConstant> constructorArgsArray,
ImmutableArray<string?> constructorArgumentNamesOpt,
AttributeSyntax syntax,
ImmutableArray<int> argumentsToParams,
BindingDiagnosticBag diagnostics,
bool expanded,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reviewers should be aware that the diff gets quite hard to read in this file from here on. You might be better off just separately reviewing the lines deleted and lines added due to large gaps in the regular split view.

@RikkiGibson RikkiGibson enabled auto-merge (squash) March 3, 2022 21:44
@RikkiGibson RikkiGibson merged commit b1b184a into dotnet:main Mar 3, 2022
@ghost ghost added this to the Next milestone Mar 3, 2022
@RikkiGibson RikkiGibson deleted the boundattribute-defaultarguments branch March 4, 2022 00:12
333fred added a commit that referenced this pull request Mar 7, 2022
…ures/required-members

* upstream/main: (78 commits)
  [main] Update dependencies from dotnet/roslyn (#59792)
  Annotate BreakpointSpans and fix NREs (#59846)
  Editor namespaces refactoring - part 1 (#59907)
  [main] Update dependencies from dotnet/source-build-externals (#59845)
  Update PublishData.json (#59952)
  ...and the comment
  Revert the last change in the compiler layer
  Force SVsExtensionManager to load before InProcComponent creation
  Handle nameof situation, moved logic back to workspaces layer
  Add back OmniSharpInlineHints (#59941)
  Initialize AsyncCompletionTracker in a fire-and-forget manner
  Try to load the extension manager from the main thread as well
  Use the correct GUID for SVsUserNotificationsService
  Update CodeStyle/format/SDK versions (#59903)
  Create attribute default arguments during binding (#59750)
  Undo refactoring part 2
  Undo refactoring
  Move logic of symbol filtering to compiling layer. Refactoring
  Clean up diagnostic and solution crawler options - take 2 (#59233)
  Add quotes in string for clarity
  ...
333fred added a commit to 333fred/roslyn that referenced this pull request Mar 15, 2022
…o enforce-setting

* upstream/features/required-members: (78 commits)
  [main] Update dependencies from dotnet/roslyn (dotnet#59792)
  Annotate BreakpointSpans and fix NREs (dotnet#59846)
  Editor namespaces refactoring - part 1 (dotnet#59907)
  [main] Update dependencies from dotnet/source-build-externals (dotnet#59845)
  Update PublishData.json (dotnet#59952)
  ...and the comment
  Revert the last change in the compiler layer
  Force SVsExtensionManager to load before InProcComponent creation
  Handle nameof situation, moved logic back to workspaces layer
  Add back OmniSharpInlineHints (dotnet#59941)
  Initialize AsyncCompletionTracker in a fire-and-forget manner
  Try to load the extension manager from the main thread as well
  Use the correct GUID for SVsUserNotificationsService
  Update CodeStyle/format/SDK versions (dotnet#59903)
  Create attribute default arguments during binding (dotnet#59750)
  Undo refactoring part 2
  Undo refactoring
  Move logic of symbol filtering to compiling layer. Refactoring
  Clean up diagnostic and solution crawler options - take 2 (dotnet#59233)
  Add quotes in string for clarity
  ...
@allisonchou allisonchou modified the milestones: Next, 17.2.P3 Mar 28, 2022
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