@@ -891,9 +891,14 @@ private static void HandleMethodCallExpression(MethodCallExpression callExpr, Di
891891 // (to avoid showing both "list" and "list[0]")
892892 ExtractVariablesFromExpression ( callExpr . Arguments [ 0 ] , details , evaluationCache , suppressIntermediateValues ) ;
893893 }
894- else if ( callExpr . Method . Name == GetMethodName && callExpr . Object is not null && callExpr . Arguments . Count > 0 )
894+ else if ( IsArrayGetCall ( callExpr ) )
895895 {
896- // Handle multi-dimensional array indexers (e.g., array.Get(i, j) displayed as array[i, j])
896+ // Handle array indexers (e.g., array.Get(i, j) displayed as array[i, j]).
897+ // In practice this only fires for multidimensional arrays — single-dimensional
898+ // arrays surface as ArrayIndex in LINQ expressions, not as a Get method call.
899+ // We gate on the receiver actually being an array so arbitrary user-defined
900+ // `Get(...)` methods on non-array types are NOT mis-rendered as `obj[...]`
901+ // (issue #6691); they go through the regular method-call path below.
897902 string objectName = GetCleanMemberName ( callExpr . Object ) ;
898903 string indexDisplay = string . Join ( ", " , callExpr . Arguments . Select ( GetIndexArgumentDisplay ) ) ;
899904 string indexerDisplay = $ "{ objectName } [{ indexDisplay } ]";
@@ -919,9 +924,10 @@ private static void HandleMethodCallExpression(MethodCallExpression callExpr, Di
919924 }
920925 else
921926 {
922- // For non-boolean methods, capture the method call itself
923- // (e.g., "list.Count" when used in a comparison)
924- string methodCallDisplay = GetCleanMemberName ( callExpr ) ;
927+ // For non-boolean methods, capture the method call itself using a friendly receiver
928+ // (issue #6691): static methods get the declaring type, captured-this instance methods
929+ // render as `this.Method(...)`, extension methods on `this` also render as `this.Method(...)`.
930+ string methodCallDisplay = GetMethodCallDisplayName ( callExpr ) ;
925931 TryAddExpressionValue ( callExpr , methodCallDisplay , details , evaluationCache ) ;
926932
927933 // Don't extract from the object to avoid duplication
@@ -935,6 +941,107 @@ private static void HandleMethodCallExpression(MethodCallExpression callExpr, Di
935941 }
936942 }
937943
944+ /// <summary>
945+ /// Matches <c>array.Get(i[, j[, k...]])</c> calls on an array receiver. In practice this only
946+ /// fires for multidimensional arrays (e.g. <c>int[,]</c>) which expose runtime-synthesized
947+ /// <c>Get</c>/<c>Set</c>/<c>Address</c> methods on the array type itself — not on
948+ /// <see cref="Array"/>; single-dimensional arrays surface as <see cref="ExpressionType.ArrayIndex"/>
949+ /// rather than a method call. Gating on the receiver actually being an array prevents arbitrary
950+ /// user-defined instance methods named <c>Get</c> from being mis-rendered as <c>obj[...]</c>
951+ /// (issue #6691).
952+ /// </summary>
953+ private static bool IsArrayGetCall ( MethodCallExpression callExpr )
954+ => callExpr . Method . Name == GetMethodName
955+ && callExpr . Object is not null
956+ && callExpr . Object . Type . IsArray
957+ && callExpr . Arguments . Count > 0 ;
958+
959+ /// <summary>
960+ /// Builds a friendly display name for a method-call expression so the failure message uses the
961+ /// same syntax the user wrote. Static methods get prefixed with their declaring type's name;
962+ /// instance methods on captured <c>this</c> render as <c>this.Method(...)</c>; extension methods
963+ /// use the first argument as the receiver. Fixes issue #6691.
964+ /// </summary>
965+ private static string GetMethodCallDisplayName ( MethodCallExpression callExpr )
966+ {
967+ string methodName = callExpr . Method . Name ;
968+
969+ // Extension methods are static methods on a static class marked [Extension]; the receiver is the
970+ // first argument. Render like the user wrote: receiver.Method(rest).
971+ if ( callExpr . Object is null
972+ && callExpr . Method . IsDefined ( typeof ( ExtensionAttribute ) , inherit : false )
973+ && callExpr . Arguments . Count > 0 )
974+ {
975+ Expression firstArg = callExpr . Arguments [ 0 ] ;
976+ Type receiverParamType = callExpr . Method . GetParameters ( ) [ 0 ] . ParameterType ;
977+ string receiver = IsCapturedThis ( firstArg , receiverParamType )
978+ ? "this"
979+ : GetCleanMemberName ( firstArg ) ;
980+ string extArgs = string . Join ( ", " , callExpr . Arguments . Skip ( 1 ) . Select ( static a => CleanExpressionText ( a . ToString ( ) ) ) ) ;
981+ return $ "{ receiver } .{ methodName } ({ extArgs } )";
982+ }
983+
984+ string argsStr = string . Join ( ", " , callExpr . Arguments . Select ( static a => CleanExpressionText ( a . ToString ( ) ) ) ) ;
985+
986+ if ( callExpr . Object is null )
987+ {
988+ // Regular static method: prefix with a friendly type display (no namespace, nested
989+ // types separated with `.` instead of the reflection `+`) so nested types keep their
990+ // nesting context (Outer.Inner.Method rather than Inner.Method).
991+ string typeName = callExpr . Method . DeclaringType is { } dt
992+ ? GetFriendlyTypeName ( dt )
993+ : NullAngleBrackets ;
994+ return $ "{ typeName } .{ methodName } ({ argsStr } )";
995+ }
996+
997+ if ( IsCapturedThis ( callExpr . Object , callExpr . Method . DeclaringType ) )
998+ {
999+ return $ "this.{ methodName } ({ argsStr } )";
1000+ }
1001+
1002+ string objectDisplay = GetCleanMemberName ( callExpr . Object ) ;
1003+ return $ "{ objectDisplay } .{ methodName } ({ argsStr } )";
1004+ }
1005+
1006+ /// <summary>
1007+ /// Returns a user-friendly display name for <paramref name="type"/>: BCL aliases via
1008+ /// <see cref="CleanTypeName(string)"/>, namespace stripped, and nested-type separators
1009+ /// (reflection's <c>+</c>) converted to <c>.</c>.
1010+ /// </summary>
1011+ private static string GetFriendlyTypeName ( Type type )
1012+ {
1013+ string raw = type . Name ;
1014+ string cleaned = CleanTypeName ( raw ) ;
1015+ if ( ! ReferenceEquals ( cleaned , raw ) )
1016+ {
1017+ return cleaned ;
1018+ }
1019+
1020+ // Walk up the nesting chain to produce Outer.Inner instead of Outer+Inner.
1021+ return type . IsNested && type . DeclaringType is { } declaring
1022+ ? $ "{ GetFriendlyTypeName ( declaring ) } .{ type . Name } "
1023+ : type . Name ;
1024+ }
1025+
1026+ /// <summary>
1027+ /// Returns <see langword="true"/> if <paramref name="objectExpr"/> is a reference to the enclosing
1028+ /// instance (<c>this</c>) — either accessed via the compiler-synthesized display-class field
1029+ /// (named like <c><>4__this</c>) or as a <see cref="ConstantExpression"/> representing
1030+ /// the enclosing instance (no-closure case). For the constant form we require the expression's
1031+ /// static type to exactly match its runtime type and be assignable to <paramref name="declaringType"/>,
1032+ /// so inherited methods on <c>this</c> still render as <c>this.Method(...)</c> without
1033+ /// mis-labeling base-typed locals as <c>this</c>.
1034+ /// </summary>
1035+ private static bool IsCapturedThis ( Expression objectExpr , Type ? declaringType )
1036+ => ( objectExpr is MemberExpression me
1037+ && me . Member . Name . StartsWith ( "<>" , StringComparison . Ordinal )
1038+ && me . Member . Name . EndsWith ( "__this" , StringComparison . Ordinal ) )
1039+ || ( declaringType is not null
1040+ && objectExpr is ConstantExpression ce
1041+ && ce . Value is not null
1042+ && ce . Type == ce . Value . GetType ( )
1043+ && declaringType . IsAssignableFrom ( ce . Type ) ) ;
1044+
9381045 /// <summary>
9391046 /// Evaluates only the *sub-children* of a writable assignment target (the Left of an Assign or
9401047 /// compound assignment, or the Operand of a Pre/Post Increment/Decrement). Walking the writable
0 commit comments