From 8d2c5c210d2a3dbdb9182f91f6162e80372edb23 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 25 Feb 2026 13:51:01 +0100 Subject: [PATCH 1/7] 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 cc0dacaa83b3b2868cecc5ab30a9c091700a2896 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 25 Feb 2026 14:50:06 +0100 Subject: [PATCH 2/7] 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 b46eaa856ce7aa112d3a531fb465b46000e41c41 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 26 Feb 2026 16:02:58 +0100 Subject: [PATCH 3/7] 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 c699415a23a22a467206ab770c6a690ba64b4109 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 26 Feb 2026 16:10:39 +0100 Subject: [PATCH 4/7] C#: Extract calls to user defined compound assignments as operator calls. --- .../Expressions/CompoundAssignment.cs | 20 ++++++++++ .../Entities/Expressions/Factory.cs | 6 ++- .../UserCompoundAssignmentInvocation.cs | 40 +++++++++++++++++++ 3 files changed, 64 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..ea8c76365234 --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/CompoundAssignment.cs @@ -0,0 +1,20 @@ +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 && + !op.IsStatic) + { + // This is a user-defined instance 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); + } + } +} From 517a8cb5bd22c7737189b7f5d7f424483116f415 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 27 Feb 2026 09:47:52 +0100 Subject: [PATCH 5/7] C#: Consider the left argument as the qualifier of instance operator calls. --- .../Entities/Expressions/UserCompoundAssignmentInvocation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs index e84e2279edbe..d6313595a3bc 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/UserCompoundAssignmentInvocation.cs @@ -23,8 +23,8 @@ protected UserCompoundAssignmentInvocation(ExpressionNodeInfo info) protected override void PopulateExpression(TextWriter trapFile) { - Create(Context, Syntax.Left, this, 0); - Create(Context, Syntax.Right, this, 1); + Create(Context, Syntax.Left, this, -1); + Create(Context, Syntax.Right, this, 0); var target = info.GetTargetSymbol(Context); if (target is null) From 5470de0b801407d21105ce7f33682d8f6a0ef701 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 5 Mar 2026 09:14:13 +0100 Subject: [PATCH 6/7] C#: Add QL implementation for the new operators. --- csharp/ql/lib/semmle/code/csharp/Callable.qll | 255 +++++++++++++++++- 1 file changed, 253 insertions(+), 2 deletions(-) diff --git a/csharp/ql/lib/semmle/code/csharp/Callable.qll b/csharp/ql/lib/semmle/code/csharp/Callable.qll index f8346cfe01e2..a9074dc1341c 100644 --- a/csharp/ql/lib/semmle/code/csharp/Callable.qll +++ b/csharp/ql/lib/semmle/code/csharp/Callable.qll @@ -496,7 +496,8 @@ class Destructor extends Callable, Member, Attributable, @destructor { * A user-defined operator. * * Either a unary operator (`UnaryOperator`), a binary operator - * (`BinaryOperator`), or a conversion operator (`ConversionOperator`). + * (`BinaryOperator`), a conversion operator (`ConversionOperator`), or + * a (`CompoundAssignmentOperator`). */ class Operator extends Callable, Member, Attributable, Overridable, @operator { override string getName() { operators(this, _, result, _, _, _) } @@ -584,7 +585,8 @@ class ExtensionOperator extends ExtensionCallableImpl, Operator { class UnaryOperator extends Operator { UnaryOperator() { this.getNumberOfParameters() = 1 and - not this instanceof ConversionOperator + not this instanceof ConversionOperator and + not this instanceof CompoundAssignmentOperator } } @@ -1157,6 +1159,255 @@ class CheckedExplicitConversionOperator extends ConversionOperator { override string getAPrimaryQlClass() { result = "CheckedExplicitConversionOperator" } } +/** + * A user-defined compound assignment operator. + * + * Either an addition operator (`AddCompoundAssignmentOperator`), a checked addition operator + * (`CheckedAddCompoundAssignmentOperator`) a subtraction operator (`SubCompoundAssignmentOperator`), a checked + * subtraction operator (`CheckedSubCompoundAssignmentOperator`), a multiplication operator + * (`MulCompoundAssignmentOperator`), a checked multiplication operator (`CheckedMulCompoundAssignmentOperator`), + * a division operator (`DivCompoundAssignmentOperator`), a checked division operator + * (`CheckedDivCompoundAssignmentOperator`), a remainder operator (`RemCompoundAssignmentOperator`), an and + * operator (`AndCompoundAssignmentOperator`), an or operator (`OrCompoundAssignmentOperator`), an xor + * operator (`XorCompoundAssignmentOperator`), a left shift operator (`LeftShiftCompoundAssignmentOperator`), + * a right shift operator (`RightShiftCompoundAssignmentOperator`), or an unsigned right shift + * operator(`UnsignedRightShiftCompoundAssignmentOperator`). + */ +class CompoundAssignmentOperator extends Operator { + CompoundAssignmentOperator() { + this.getNumberOfParameters() = 1 and + not this.isStatic() and + this.getName().matches("%=") + } + + override string getAPrimaryQlClass() { result = "CompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment addition operator (`+=`), for example + * + * ```csharp + * public void operator checked +=(Widget w) { + * ... + * } + * ``` + */ +class AddCompoundAssignmentOperator extends CompoundAssignmentOperator { + AddCompoundAssignmentOperator() { this.getName() = "+=" } + + override string getAPrimaryQlClass() { result = "AddCompoundAssignmentOperator" } +} + +/** + * A user-defined checked compound assignment addition operator (`checked +=`), for example + * + * ```csharp + * public void operator checked +=(Widget w) { + * ... + * } + * ``` + */ +class CheckedAddCompoundAssignmentOperator extends CompoundAssignmentOperator { + CheckedAddCompoundAssignmentOperator() { this.getName() = "checked +=" } + + override string getAPrimaryQlClass() { result = "CheckedAddCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment subtraction operator (`-=`), for example + * + * ```csharp + * public void operator -=(Widget w) { + * ... + * } + * ``` + */ +class SubCompoundAssignmentOperator extends CompoundAssignmentOperator { + SubCompoundAssignmentOperator() { this.getName() = "-=" } + + override string getAPrimaryQlClass() { result = "SubCompoundAssignmentOperator" } +} + +/** + * A user-defined checked compound assignment subtraction operator (`checked -=`), for example + * + * ```csharp + * public void operator checked -=(Widget w) { + * ... + * } + * ``` + */ +class CheckedSubCompoundAssignmentOperator extends CompoundAssignmentOperator { + CheckedSubCompoundAssignmentOperator() { this.getName() = "checked -=" } + + override string getAPrimaryQlClass() { result = "CheckedSubCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment multiplication operator (`*=`), for example + * + * ```csharp + * public void operator *=(Widget w) { + * ... + * } + * ``` + */ +class MulCompoundAssignmentOperator extends CompoundAssignmentOperator { + MulCompoundAssignmentOperator() { this.getName() = "*=" } + + override string getAPrimaryQlClass() { result = "MulCompoundAssignmentOperator" } +} + +/** + * A user-defined checked compound assignment multiplication operator (`checked *=`), for example + * + * ```csharp + * public void operator checked *=(Widget w) { + * ... + * } + * ``` + */ +class CheckedMulCompoundAssignmentOperator extends CompoundAssignmentOperator { + CheckedMulCompoundAssignmentOperator() { this.getName() = "checked *=" } + + override string getAPrimaryQlClass() { result = "CheckedMulCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment division operator (`/=`), for example + * + * ```csharp + * public void operator /=(Widget w) { + * ... + * } + * ``` + */ +class DivCompoundAssignmentOperator extends CompoundAssignmentOperator { + DivCompoundAssignmentOperator() { this.getName() = "/=" } + + override string getAPrimaryQlClass() { result = "DivCompoundAssignmentOperator" } +} + +/** + * A user-defined checked compound assignment division operator (`checked /=`), for example + * + * ```csharp + * public void operator checked /=(Widget w) { + * ... + * } + * ``` + */ +class CheckedDivCompoundAssignmentOperator extends CompoundAssignmentOperator { + CheckedDivCompoundAssignmentOperator() { this.getName() = "checked /=" } + + override string getAPrimaryQlClass() { result = "CheckedDivCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment remainder operator (`%=`), for example + * + * ```csharp + * public void operator %=(Widget w) { + * ... + * } + * ``` + */ +class RemCompoundAssignmentOperator extends CompoundAssignmentOperator { + RemCompoundAssignmentOperator() { this.getName() = "%=" } + + override string getAPrimaryQlClass() { result = "RemCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment and operator (`&=`), for example + * + * ```csharp + * public void operator &=(Widget w) { + * ... + * } + * ``` + */ +class AndCompoundAssignmentOperator extends CompoundAssignmentOperator { + AndCompoundAssignmentOperator() { this.getName() = "&=" } + + override string getAPrimaryQlClass() { result = "AndCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment or operator (`|=`), for example + * + * ```csharp + * public void operator |=(Widget w) { + * ... + * } + * ``` + */ +class OrCompoundAssignmentOperator extends CompoundAssignmentOperator { + OrCompoundAssignmentOperator() { this.getName() = "|=" } + + override string getAPrimaryQlClass() { result = "OrCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment xor operator (`^=`), for example + * + * ```csharp + * public void operator ^=(Widget w) { + * ... + * } + * ``` + */ +class XorCompoundAssignmentOperator extends CompoundAssignmentOperator { + XorCompoundAssignmentOperator() { this.getName() = "^=" } + + override string getAPrimaryQlClass() { result = "XorCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment left shift operator (`<<=`), for example + * + * ```csharp + * public void operator <<=(Widget w) { + * ... + * } + * ``` + */ +class LeftShiftCompoundAssignmentOperator extends CompoundAssignmentOperator { + LeftShiftCompoundAssignmentOperator() { this.getName() = "<<=" } + + override string getAPrimaryQlClass() { result = "LeftShiftCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment right shift operator (`>>=`), for example + * + * ```csharp + * public void operator >>=(Widget w) { + * ... + * } + * ``` + */ +class RightShiftCompoundAssignmentOperator extends CompoundAssignmentOperator { + RightShiftCompoundAssignmentOperator() { this.getName() = ">>=" } + + override string getAPrimaryQlClass() { result = "RightShiftCompoundAssignmentOperator" } +} + +/** + * A user-defined compound assignment unsigned right shift operator (`>>>=`), for example + * + * ```csharp + * public void operator >>>=(Widget w) { + * ... + * } + * ``` + */ +class UnsignedRightShiftCompoundAssignmentOperator extends CompoundAssignmentOperator { + UnsignedRightShiftCompoundAssignmentOperator() { this.getName() = ">>>=" } + + override string getAPrimaryQlClass() { result = "UnsignedRightShiftCompoundAssignmentOperator" } +} + /** * A local function, defined within the scope of another callable. * For example, `Fac` on lines 2--4 in From 07345fed4b48e1932b082ee68af77dc529ae08e8 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 5 Mar 2026 09:47:42 +0100 Subject: [PATCH 7/7] C#: Add compound assignment operator call class. --- .../ql/lib/semmle/code/csharp/exprs/Call.qll | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll b/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll index f8b51a990ed1..94087b47f88d 100644 --- a/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll +++ b/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll @@ -553,6 +553,28 @@ class MutatorOperatorCall extends OperatorCall { predicate isPostfix() { mutator_invocation_mode(this, 2) } } +/** + * A call to a user-defined compound assignment operator, for example `this += other` + * line 7 in + * + * ```csharp + * class A { + * public void operator+=(A other) { + * ... + * } + * + * public A Add(A other) { + * return this += other; + * } + * } + * ``` + */ +class CompoundAssignmentOperatorCall extends OperatorCall { + CompoundAssignmentOperatorCall() { this.getTarget() instanceof CompoundAssignmentOperator } + + override string getAPrimaryQlClass() { result = "CompoundAssignmentOperatorCall" } +} + private class DelegateLikeCall_ = @delegate_invocation_expr or @function_pointer_invocation_expr; /**