Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 48 additions & 98 deletions csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;

Expand All @@ -18,114 +19,63 @@ public static string GetName(this ISymbol symbol, bool useMetadataName = false)
return symbol.CanBeReferencedByName ? name : name.Substring(symbol.Name.LastIndexOf('.') + 1);
}

private static readonly Dictionary<string, string> methodToOperator = new Dictionary<string, string>
{
{ "op_LogicalNot", "!" },
{ "op_BitwiseAnd", "&" },
{ "op_Equality", "==" },
{ "op_Inequality", "!=" },
{ "op_UnaryPlus", "+" },
{ "op_Addition", "+" },
{ "op_UnaryNegation", "-" },
{ "op_Subtraction", "-" },
{ "op_Multiply", "*" },
{ "op_Division", "/" },
{ "op_Modulus", "%" },
{ "op_GreaterThan", ">" },
{ "op_GreaterThanOrEqual", ">=" },
{ "op_LessThan", "<" },
{ "op_LessThanOrEqual", "<=" },
{ "op_Decrement", "--" },
{ "op_Increment", "++" },
{ "op_Implicit", "implicit conversion" },
{ "op_Explicit", "explicit conversion" },
{ "op_OnesComplement", "~" },
{ "op_RightShift", ">>" },
{ "op_UnsignedRightShift", ">>>" },
{ "op_LeftShift", "<<" },
{ "op_BitwiseOr", "|" },
{ "op_ExclusiveOr", "^" },
{ "op_True", "true" },
{ "op_False", "false" }
};

/// <summary>
/// Convert an operator method name in to a symbolic name.
/// A return value indicates whether the conversion succeeded.
/// </summary>
public static bool TryGetOperatorSymbol(this ISymbol symbol, out string operatorName)
{
static bool TryGetOperatorSymbolFromName(string methodName, out string operatorName)
var methodName = symbol.GetName(useMetadataName: false);

// Most common use-case.
if (methodToOperator.TryGetValue(methodName, out operatorName!))
return true;

// Attempt to parse using a regexp.
var match = OperatorRegex().Match(methodName);
if (match.Success && methodToOperator.TryGetValue($"op_{match.Groups[2]}", out var rawOperatorName))
{
var success = true;
switch (methodName)
{
case "op_LogicalNot":
operatorName = "!";
break;
case "op_BitwiseAnd":
operatorName = "&";
break;
case "op_Equality":
operatorName = "==";
break;
case "op_Inequality":
operatorName = "!=";
break;
case "op_UnaryPlus":
case "op_Addition":
operatorName = "+";
break;
case "op_UnaryNegation":
case "op_Subtraction":
operatorName = "-";
break;
case "op_Multiply":
operatorName = "*";
break;
case "op_Division":
operatorName = "/";
break;
case "op_Modulus":
operatorName = "%";
break;
case "op_GreaterThan":
operatorName = ">";
break;
case "op_GreaterThanOrEqual":
operatorName = ">=";
break;
case "op_LessThan":
operatorName = "<";
break;
case "op_LessThanOrEqual":
operatorName = "<=";
break;
case "op_Decrement":
operatorName = "--";
break;
case "op_Increment":
operatorName = "++";
break;
case "op_Implicit":
operatorName = "implicit conversion";
break;
case "op_Explicit":
operatorName = "explicit conversion";
break;
case "op_OnesComplement":
operatorName = "~";
break;
case "op_RightShift":
operatorName = ">>";
break;
case "op_UnsignedRightShift":
operatorName = ">>>";
break;
case "op_LeftShift":
operatorName = "<<";
break;
case "op_BitwiseOr":
operatorName = "|";
break;
case "op_ExclusiveOr":
operatorName = "^";
break;
case "op_True":
operatorName = "true";
break;
case "op_False":
operatorName = "false";
break;
default:
var match = CheckedRegex().Match(methodName);
if (match.Success)
{
TryGetOperatorSymbolFromName($"op_{match.Groups[1]}", out var uncheckedName);
operatorName = $"checked {uncheckedName}";
break;
}
operatorName = methodName;
success = false;
break;
}
return success;
var prefix = match.Groups[1].Success ? "checked " : "";
var postfix = match.Groups[3].Success ? "=" : "";
operatorName = $"{prefix}{rawOperatorName}{postfix}";
return true;
}

var methodName = symbol.GetName(useMetadataName: false);
return TryGetOperatorSymbolFromName(methodName, out operatorName);
return false;
}

[GeneratedRegex("^op_Checked(.*)$")]
private static partial Regex CheckedRegex();
[GeneratedRegex("^op_(Checked)?(.*?)(Assignment)?$")]
private static partial Regex OperatorRegex();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Util;
using Semmle.Extraction.CSharp.Entities;

Expand Down Expand Up @@ -856,5 +857,34 @@ public static Parameter.Kind GetParameterKind(this IParameterSymbol parameter)
return Parameter.Kind.None;
}
}

public static IMethodSymbol? GetTargetSymbol(this ExpressionNodeInfo info, Context cx)
{
var si = info.SymbolInfo;
if (si.Symbol is ISymbol symbol)
{
var method = symbol as IMethodSymbol;
// Case for compiler-generated extension methods.
return method?.TryGetExtensionMethod() ?? method;
}

if (si.CandidateReason == CandidateReason.OverloadResolutionFailure && info.Node is InvocationExpressionSyntax syntax)
{
// This seems to be a bug in Roslyn
// For some reason, typeof(X).InvokeMember(...) fails to resolve the correct
// InvokeMember() method, even though the number of parameters clearly identifies the correct method

var candidates = si.CandidateSymbols
.OfType<IMethodSymbol>()
.Where(method => method.Parameters.Length >= syntax.ArgumentList.Arguments.Count)
.Where(method => method.Parameters.Count(p => !p.HasExplicitDefaultValue) <= syntax.ArgumentList.Arguments.Count);

return cx.ExtractionContext.IsStandalone ?
candidates.FirstOrDefault() :
candidates.SingleOrDefault();
}

return si.Symbol as IMethodSymbol;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.CodeAnalysis;

namespace Semmle.Extraction.CSharp.Entities.Expressions
{
internal static class CompoundAssignment
{
public static Expression Create(ExpressionNodeInfo info)
{
if (info.SymbolInfo.Symbol is IMethodSymbol op && op.MethodKind == MethodKind.UserDefinedOperator)
{
// This is a user-defined operator such as `a += b` where `a` is of a type that defines an `operator +=`.
// In this case, we want to extract the operator call rather than desugar it into `a = a + b`.
return UserCompoundAssignmentInvocation.Create(info);
}
return Assignment.Create(info);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ internal static Expression Create(ExpressionNodeInfo info)
return NormalElementAccess.Create(info);

case SyntaxKind.SimpleAssignmentExpression:
case SyntaxKind.CoalesceAssignmentExpression:
return Assignment.Create(info);

case SyntaxKind.OrAssignmentExpression:
case SyntaxKind.AndAssignmentExpression:
case SyntaxKind.SubtractAssignmentExpression:
Expand All @@ -81,8 +84,7 @@ internal static Expression Create(ExpressionNodeInfo info)
case SyntaxKind.UnsignedRightShiftAssignmentExpression:
case SyntaxKind.DivideAssignmentExpression:
case SyntaxKind.ModuloAssignmentExpression:
case SyntaxKind.CoalesceAssignmentExpression:
return Assignment.Create(info);
return CompoundAssignment.Create(info);

case SyntaxKind.ObjectCreationExpression:
return ExplicitObjectCreation.Create(info);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected override void PopulateExpression(TextWriter trapFile)

var child = -1;
string? memberName = null;
var target = TargetSymbol;
var target = info.GetTargetSymbol(Context);
switch (Syntax.Expression)
{
case MemberAccessExpressionSyntax memberAccess when IsValidMemberAccessKind():
Expand Down Expand Up @@ -129,39 +129,6 @@ private static bool IsOperatorLikeCall(ExpressionNodeInfo info)
method.TryGetExtensionMethod()?.MethodKind == MethodKind.UserDefinedOperator;
}

public IMethodSymbol? TargetSymbol
{
get
{
var si = SymbolInfo;

if (si.Symbol is ISymbol symbol)
{
var method = symbol as IMethodSymbol;
// Case for compiler-generated extension methods.
return method?.TryGetExtensionMethod() ?? method;
}

if (si.CandidateReason == CandidateReason.OverloadResolutionFailure)
{
// This seems to be a bug in Roslyn
// For some reason, typeof(X).InvokeMember(...) fails to resolve the correct
// InvokeMember() method, even though the number of parameters clearly identifies the correct method

var candidates = si.CandidateSymbols
.OfType<IMethodSymbol>()
.Where(method => method.Parameters.Length >= Syntax.ArgumentList.Arguments.Count)
.Where(method => method.Parameters.Count(p => !p.HasExplicitDefaultValue) <= Syntax.ArgumentList.Arguments.Count);

return Context.ExtractionContext.IsStandalone ?
candidates.FirstOrDefault() :
candidates.SingleOrDefault();
}

return si.Symbol as IMethodSymbol;
}
}

private static bool IsDelegateLikeCall(ExpressionNodeInfo info)
{
return IsDelegateLikeCall(info, symbol => IsFunctionPointer(symbol) || IsDelegateInvoke(symbol));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.IO;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Kinds;

namespace Semmle.Extraction.CSharp.Entities.Expressions
{
/// <summary>
/// Represents a user-defined compound assignment operator such as `a += b` where `a` is of a type that defines an `operator +=`.
/// In this case, we don't want to desugar it into `a = a + b`, but instead extract the operator call directly as it should
/// be considered an instance method call on `a` with `b` as an argument.
/// </summary>
internal class UserCompoundAssignmentInvocation : Expression<AssignmentExpressionSyntax>
{
private readonly ExpressionNodeInfo info;

protected UserCompoundAssignmentInvocation(ExpressionNodeInfo info)
: base(info.SetKind(ExprKind.OPERATOR_INVOCATION))
{
this.info = info;
}

public static Expression Create(ExpressionNodeInfo info) => new UserCompoundAssignmentInvocation(info).TryPopulate();

protected override void PopulateExpression(TextWriter trapFile)
{
Create(Context, Syntax.Left, this, -1);
Create(Context, Syntax.Right, this, 0);

var target = info.GetTargetSymbol(Context);
if (target is null)
{
Context.ModelError(Syntax, "Unable to resolve target method for user-defined compound assignment operator");
return;
}

var targetKey = Method.Create(Context, target);
trapFile.expr_call(this, targetKey);
}
}
}