dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.93k stars 4.02k forks source link

IOperation: arguments of dynamic invocation are not visited #27676

Open bkoelman opened 6 years ago

bkoelman commented 6 years ago

Version Used: NuGet Microsoft.CodeAnalysis v2.8.2

Steps to Reproduce: Consider the next program, which prints visited arguments:

using System;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;

namespace RoslynDynamicArgumentBug
{
    class Program
    {
        static void Main()
        {
            const string source = @"
namespace N
{
    class C
    {
        void M(dynamic d)
        {
            d.X(1, 2, 3);

            N(4, 5, 6);
        }

        void N(int i, int j, int k) => throw null;
    }
}";

            SyntaxTree tree = CSharpSyntaxTree.ParseText(source);
            var compilation = CSharpCompilation.Create("test.dll", new[] { tree });
            var model = compilation.GetSemanticModel(tree);

            var methodSyntax = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>().First();

            var block = (IBlockOperation)model.GetOperation(methodSyntax.Body);

            var walker = new ArgumentWalker();
            walker.Visit(block);
        }

        private sealed class ArgumentWalker : OperationWalker
        {
            public override void VisitArgument(IArgumentOperation operation)
            {
                Console.WriteLine(operation.Syntax.ToString());

                base.VisitArgument(operation);
            }
        }
    }
}

Expected Behavior: Output 1, 2, 3, 4, 5, 6

Actual Behavior: Output 4, 5, 6

jcouv commented 6 years ago

Tagging @AlekseyTs for triage

AlekseyTs commented 6 years ago

I do not think IArgumentOperation node is used for dynamic invocations. This is purely early bound node with information that cannot be provided for dynamic/late bound operations. Looks by design to me.

bkoelman commented 6 years ago

I found a case where the argument to a dynamic invocation is actually visited. To repro, replace the source text with:

namespace N
{
    class C
    {
        delegate int F(int x, params object[] args);

        private F f;

        void M(dynamic d)
        {
            d.X(1, 2, x: f(0));

            N(4, 5, 6);
        }

        void N(int i, int j, int k) => throw null;
    }
}

and rerun, which outputs:

0
x: f(0)
4
5
6
AlekseyTs commented 6 years ago

I found a case where the argument to a dynamic invocation is actually visited.

Please make sure to examine the actual IOperation tree you are dealing with. As far as I can see, none of the immediate children of IDynamicInvocationOperation node are IArgumentOperations, there are two IArgumentOperation involved into delegate invocation f(0), but this is not a dynamic invocation.


        [CompilerTrait(CompilerFeature.IOperation)]
        [Fact]
        public void Issue27676()
        {
            string source = @"
namespace N
{
    class C
    {
        delegate int F(int x, params object[] args);

        private F f;

        void M(dynamic d)
        {
            /*<bind>*/d.X(1, 2, x: f(0))/*</bind>*/;

            N(4, 5, 6);
        }

        void N(int i, int j, int k) => throw null;
    }
}
";
            string expectedOperationTree = @"
IDynamicInvocationOperation (OperationKind.DynamicInvocation, Type: dynamic) (Syntax: 'd.X(1, 2, x: f(0))')
  Expression: 
    IDynamicMemberReferenceOperation (Member Name: ""X"", Containing Type: null) (OperationKind.DynamicMemberReference, Type: dynamic) (Syntax: 'd.X')
      Type Arguments(0)
      Instance Receiver: 
        IParameterReferenceOperation: d (OperationKind.ParameterReference, Type: dynamic) (Syntax: 'd')
  Arguments(3):
      ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1) (Syntax: '1')
      ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 2) (Syntax: '2')
      IInvocationOperation (virtual System.Int32 N.C.F.Invoke(System.Int32 x, params System.Object[] args)) (OperationKind.Invocation, Type: System.Int32) (Syntax: 'f(0)')
        Instance Receiver: 
          IFieldReferenceOperation: N.C.F N.C.f (OperationKind.FieldReference, Type: N.C.F) (Syntax: 'f')
            Instance Receiver: 
              IInstanceReferenceOperation (ReferenceKind: ContainingTypeInstance) (OperationKind.InstanceReference, Type: N.C, IsImplicit) (Syntax: 'f')
        Arguments(2):
            IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: x) (OperationKind.Argument, Type: null) (Syntax: '0')
              ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 0) (Syntax: '0')
              InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null)
              OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null)
            IArgumentOperation (ArgumentKind.ParamArray, Matching Parameter: args) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'x: f(0)')
              IArrayCreationOperation (OperationKind.ArrayCreation, Type: System.Object[], IsImplicit) (Syntax: 'f(0)')
                Dimension Sizes(1):
                    ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 0, IsImplicit) (Syntax: 'f(0)')
                Initializer: 
                  IArrayInitializerOperation (0 elements) (OperationKind.ArrayInitializer, Type: null, IsImplicit) (Syntax: 'f(0)')
                    Element Values(0)
              InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null)
              OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null)
  ArgumentNames(3):
    ""null""
    ""null""
    ""x""
  ArgumentRefKinds(0)
";
            var expectedDiagnostics = new[] {
                // file.cs(8,19): warning CS0649: Field 'C.f' is never assigned to, and will always have its default value null
                //         private F f;
                Diagnostic(ErrorCode.WRN_UnassignedInternalField, "f").WithArguments("N.C.f", "null").WithLocation(8, 19)
            };

            VerifyOperationTreeAndDiagnosticsForTest<InvocationExpressionSyntax>(source, expectedOperationTree, expectedDiagnostics);
        }
bkoelman commented 6 years ago

Aha, I see now. Another compiler-generated operation with an unexpected syntax location. Well, the chosen location makes sense, given that all operations must have backing syntax. Just not having a location for compiler-generated code feels more appropriate to be as an outsider. But there are probably reasons I am unaware of to have the current design, so lets forget about that.

Just out of curiosity: Is there a way to produce that operation dump as a Microsoft.CodeAnalysis NuGet consumer (not being at work inside the roslyn codebase)?

Getting back to the original issue: I understand that IArgumentOperation.Parameter and the In/Out conversions make no sense for dynamic invocations. So lets change this issue into a request to add operation support for visiting dynamic arguments then.