From 57d62758c721fdad6fce7e7ee7a5b171366b79e6 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 25 Feb 2026 13:51:01 +0100 Subject: [PATCH 1/4] C#: Use a dictionary for translating operator methods to operator symbols. --- .../SymbolExtensions.cs | 139 ++++++------------ 1 file changed, 42 insertions(+), 97 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs b/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs index 659b26c2fe99..d2c0a1ecec0b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; @@ -18,111 +19,55 @@ public static string GetName(this ISymbol symbol, bool useMetadataName = false) return symbol.CanBeReferencedByName ? name : name.Substring(symbol.Name.LastIndexOf('.') + 1); } + private static readonly Dictionary methodToOperator = new Dictionary + { + { "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" } + }; + /// /// Convert an operator method name in to a symbolic name. /// A return value indicates whether the conversion succeeded. /// public static bool TryGetOperatorSymbol(this ISymbol symbol, out string operatorName) { - static bool TryGetOperatorSymbolFromName(string methodName, out string operatorName) + var methodName = symbol.GetName(useMetadataName: false); + + if (methodToOperator.TryGetValue(methodName, out operatorName!)) + return true; + + var match = CheckedRegex().Match(methodName); + if (match.Success && methodToOperator.TryGetValue($"op_{match.Groups[1]}", out var uncheckedName)) { - 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; + operatorName = $"checked {uncheckedName}"; + return true; } - - var methodName = symbol.GetName(useMetadataName: false); - return TryGetOperatorSymbolFromName(methodName, out operatorName); + return false; } [GeneratedRegex("^op_Checked(.*)$")] From 4f84272e78b32e84f39a283f469c6ad98cd14526 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 25 Feb 2026 14:50:06 +0100 Subject: [PATCH 2/4] C#: Support user defined compound assignment operators. --- .../SymbolExtensions.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs b/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs index d2c0a1ecec0b..bf2aeb2bb650 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.Util/SymbolExtensions.cs @@ -58,19 +58,24 @@ public static bool TryGetOperatorSymbol(this ISymbol symbol, out string operator { var methodName = symbol.GetName(useMetadataName: false); + // Most common use-case. if (methodToOperator.TryGetValue(methodName, out operatorName!)) return true; - var match = CheckedRegex().Match(methodName); - if (match.Success && methodToOperator.TryGetValue($"op_{match.Groups[1]}", out var uncheckedName)) + // Attempt to parse using a regexp. + var match = OperatorRegex().Match(methodName); + if (match.Success && methodToOperator.TryGetValue($"op_{match.Groups[2]}", out var rawOperatorName)) { - operatorName = $"checked {uncheckedName}"; + var prefix = match.Groups[1].Success ? "checked " : ""; + var postfix = match.Groups[3].Success ? "=" : ""; + operatorName = $"{prefix}{rawOperatorName}{postfix}"; return true; } + return false; } - [GeneratedRegex("^op_Checked(.*)$")] - private static partial Regex CheckedRegex(); + [GeneratedRegex("^op_(Checked)?(.*?)(Assignment)?$")] + private static partial Regex OperatorRegex(); } } From 75e39e0dea2856275592106cfc54a63a34cb5b80 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 26 Feb 2026 16:02:58 +0100 Subject: [PATCH 3/4] C#: Re-factor TargetSymbol into an extension method. --- .../SymbolExtensions.cs | 30 ++++++++++++++++ .../Entities/Expressions/Invocation.cs | 35 +------------------ 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs b/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs index fbc1b52c99b3..39fe8a8eca95 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs @@ -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; @@ -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() + .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; + } } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs index 2ed7aec9955c..b5f06f20e58e 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Invocation.cs @@ -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(): @@ -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() - .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)); From c06843e38a078404035a1a4423e1dfb176365035 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 26 Feb 2026 16:10:39 +0100 Subject: [PATCH 4/4] C#: Extract calls to user defined compound assignments as operator calls. --- .../Expressions/CompoundAssignment.cs | 18 +++++++++ .../Entities/Expressions/Factory.cs | 6 ++- .../UserCompoundAssignmentInvocation.cs | 40 +++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/CompoundAssignment.cs create mode 100644 csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/CompoundAssignment.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/CompoundAssignment.cs new file mode 100644 index 000000000000..ebe6e7f24790 --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/CompoundAssignment.cs @@ -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); + } + } +} diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Factory.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Factory.cs index ed8dae3738fc..b03442936b21 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Factory.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Factory.cs @@ -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: @@ -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); diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs new file mode 100644 index 000000000000..e84e2279edbe --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs @@ -0,0 +1,40 @@ +using System.IO; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Semmle.Extraction.Kinds; + +namespace Semmle.Extraction.CSharp.Entities.Expressions +{ + /// + /// 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. + /// + internal class UserCompoundAssignmentInvocation : Expression + { + 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, 0); + Create(Context, Syntax.Right, this, 1); + + 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); + } + } +}