diff --git a/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs b/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs
index bb61efe332c8..cb1f36f8a2de 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp/CodeAnalysisExtensions/SymbolExtensions.cs
@@ -532,6 +532,12 @@ attribute.AttributeClass is INamedTypeSymbol nt &&
return isInline;
}
+ ///
+ /// Returns true if this type implements `System.IFormattable`.
+ ///
+ public static bool ImplementsIFormattable(this ITypeSymbol type) =>
+ type.AllInterfaces.Any(i => i.Name == "IFormattable" && i.ContainingNamespace.ToString() == "System");
+
///
/// Holds if this type is of the form System.ReadOnlySpan<byte>
.
///
diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expression.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expression.cs
index 9241528eb752..a5cb6e316f41 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expression.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expression.cs
@@ -129,7 +129,7 @@ public static void CreateDeferred(Context cx, ExpressionSyntax node, IExpression
cx.PopulateLater(() => Create(cx, node, parent, child));
}
- private static bool ContainsPattern(SyntaxNode node) =>
+ protected static bool ContainsPattern(SyntaxNode node) =>
node is PatternSyntax || node is VariableDesignationSyntax || node.ChildNodes().Any(ContainsPattern);
///
diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/ExpressionNodeInfo.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/ExpressionNodeInfo.cs
index 924382a55507..5a2cad8a520a 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/ExpressionNodeInfo.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/ExpressionNodeInfo.cs
@@ -129,7 +129,13 @@ public Location Location
public ExprKind Kind { get; set; } = ExprKind.UNKNOWN;
- public bool IsCompilerGenerated { get; set; }
+ public bool IsCompilerGenerated { get; init; }
+
+ ///
+ /// Whether the expression should have a compiler generated `ToString` call added,
+ /// if there is no suitable implicit cast.
+ ///
+ public bool ImplicitToString { get; private set; }
public ExpressionNodeInfo SetParent(IExpressionParentEntity parent, int child)
{
@@ -157,6 +163,12 @@ public ExpressionNodeInfo SetNode(ExpressionSyntax node)
return this;
}
+ public ExpressionNodeInfo SetImplicitToString(bool value)
+ {
+ ImplicitToString = value;
+ return this;
+ }
+
private SymbolInfo cachedSymbolInfo;
public SymbolInfo SymbolInfo
diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Binary.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Binary.cs
index 3cdfb32277b2..eeb1b9ba63b2 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Binary.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/Binary.cs
@@ -14,11 +14,35 @@ private Binary(ExpressionNodeInfo info)
public static Expression Create(ExpressionNodeInfo info) => new Binary(info).TryPopulate();
+ private Expression CreateChild(Context cx, ExpressionSyntax node, int child)
+ {
+ // If this is a "+" expression we might need to wrap the child expressions
+ // in ToString calls
+ return Kind == ExprKind.ADD
+ ? ImplicitToString.Create(cx, node, this, child)
+ : Create(cx, node, this, child);
+ }
+
+ ///
+ /// Creates an expression from a syntax node.
+ /// Inserts type conversion as required.
+ /// Population is deferred to avoid overflowing the stack.
+ ///
+ private void CreateDeferred(Context cx, ExpressionSyntax node, int child)
+ {
+ if (ContainsPattern(node))
+ // Expressions with patterns should be created right away, as they may introduce
+ // local variables referenced in `LocalVariable::GetAlreadyCreated()`
+ CreateChild(cx, node, child);
+ else
+ cx.PopulateLater(() => CreateChild(cx, node, child));
+ }
+
protected override void PopulateExpression(TextWriter trapFile)
{
OperatorCall(trapFile, Syntax);
- CreateDeferred(Context, Syntax.Left, this, 0);
- CreateDeferred(Context, Syntax.Right, this, 1);
+ CreateDeferred(Context, Syntax.Left, 0);
+ CreateDeferred(Context, Syntax.Right, 1);
}
private static ExprKind GetKind(Context cx, BinaryExpressionSyntax node)
diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitCast.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitCast.cs
index b0508bf83b7e..c1ce5dcbfe99 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitCast.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitCast.cs
@@ -156,6 +156,12 @@ convertedType.Symbol is IPointerTypeSymbol &&
return new ImplicitCast(info);
}
+ if (info.ImplicitToString)
+ {
+ // x -> x.ToString() in "abc" + x
+ return ImplicitToString.Wrap(info);
+ }
+
// Default: Just create the expression without a conversion.
return Factory.Create(info);
}
diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitToString.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitToString.cs
new file mode 100644
index 000000000000..f424e98a7a51
--- /dev/null
+++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/ImplicitToString.cs
@@ -0,0 +1,59 @@
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Semmle.Extraction.CSharp.Util;
+using Semmle.Extraction.Kinds;
+
+
+namespace Semmle.Extraction.CSharp.Entities.Expressions
+{
+ internal sealed class ImplicitToString : Expression
+ {
+ ///
+ /// Gets the `ToString` method for the given type.
+ ///
+ private static IMethodSymbol? GetToStringMethod(ITypeSymbol? type)
+ {
+ return type?
+ .GetMembers()
+ .OfType()
+ .Where(method =>
+ method.GetName() == "ToString" &&
+ method.Parameters.Length == 0
+ )
+ .FirstOrDefault();
+ }
+
+ private ImplicitToString(ExpressionNodeInfo info, IMethodSymbol toString) : base(new ExpressionInfo(info.Context, AnnotatedTypeSymbol.CreateNotAnnotated(toString.ReturnType), info.Location, ExprKind.METHOD_INVOCATION, info.Parent, info.Child, isCompilerGenerated: true, info.ExprValue))
+ {
+ Factory.Create(info.SetParent(this, -1));
+
+ var target = Method.Create(Context, toString);
+ Context.TrapWriter.Writer.expr_call(this, target);
+ }
+
+ private static bool IsStringType(AnnotatedTypeSymbol? type) =>
+ type.HasValue && type.Value.Symbol?.SpecialType == SpecialType.System_String;
+
+ ///
+ /// Creates a new expression, adding a compiler generated `ToString` call if required.
+ ///
+ public static Expression Create(Context cx, ExpressionSyntax node, Expression parent, int child)
+ {
+ var info = new ExpressionNodeInfo(cx, node, parent, child);
+ return CreateFromNode(info.SetImplicitToString(IsStringType(parent.Type) && !IsStringType(info.Type)));
+ }
+
+ ///
+ /// Wraps the resulting expression in a `ToString` call, if a suitable `ToString` method is available.
+ ///
+ public static Expression Wrap(ExpressionNodeInfo info)
+ {
+ if (GetToStringMethod(info.Type?.Symbol) is IMethodSymbol toString)
+ {
+ return new ImplicitToString(info, toString);
+ }
+ return Factory.Create(info);
+ }
+ }
+}
diff --git a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/InterpolatedString.cs b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/InterpolatedString.cs
index 2dfe4976391d..6d17d1e7f176 100644
--- a/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/InterpolatedString.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp/Entities/Expressions/InterpolatedString.cs
@@ -1,4 +1,5 @@
using System.IO;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Semmle.Extraction.Kinds;
@@ -20,7 +21,15 @@ protected override void PopulateExpression(TextWriter trapFile)
{
case SyntaxKind.Interpolation:
var interpolation = (InterpolationSyntax)c;
- Create(Context, interpolation.Expression, this, child++);
+ var exp = interpolation.Expression;
+ if (Context.GetTypeInfo(exp).Type is ITypeSymbol type && !type.ImplementsIFormattable())
+ {
+ ImplicitToString.Create(Context, exp, this, child++);
+ }
+ else
+ {
+ Create(Context, exp, this, child++);
+ }
break;
case SyntaxKind.InterpolatedStringText:
// Create a string literal
diff --git a/csharp/ql/examples/snippets/ternary_conditional.ql b/csharp/ql/examples/snippets/ternary_conditional.ql
index 22e26e71ea8f..009c56fb2afe 100644
--- a/csharp/ql/examples/snippets/ternary_conditional.ql
+++ b/csharp/ql/examples/snippets/ternary_conditional.ql
@@ -11,7 +11,7 @@ import csharp
from ConditionalExpr e
where
- e.getThen().stripImplicitCasts() != e.getElse().stripImplicitCasts() and
+ e.getThen().stripImplicit() != e.getElse().stripImplicit() and
not e.getThen().getType() instanceof NullType and
not e.getElse().getType() instanceof NullType
select e
diff --git a/csharp/ql/lib/change-notes/2025-01-09-implicit-to-string.md b/csharp/ql/lib/change-notes/2025-01-09-implicit-to-string.md
new file mode 100644
index 000000000000..2956898841ee
--- /dev/null
+++ b/csharp/ql/lib/change-notes/2025-01-09-implicit-to-string.md
@@ -0,0 +1,4 @@
+---
+category: minorAnalysis
+---
+* Added extractor support for extracting implicit `ToString` calls in binary `+` expressions and string interpolation expressions.
diff --git a/csharp/ql/lib/ext/Microsoft.AspNetCore.Http.model.yml b/csharp/ql/lib/ext/Microsoft.AspNetCore.Http.model.yml
new file mode 100644
index 000000000000..b4716f100a2e
--- /dev/null
+++ b/csharp/ql/lib/ext/Microsoft.AspNetCore.Http.model.yml
@@ -0,0 +1,6 @@
+extensions:
+ - addsTo:
+ pack: codeql/csharp-all
+ extensible: summaryModel
+ data:
+ - ["Microsoft.AspNetCore.Http", "PathString", True, "ToString", "()", "", "Argument[this]", "ReturnValue", "taint", "manual"]
diff --git a/csharp/ql/lib/semmle/code/csharp/PrintAst.qll b/csharp/ql/lib/semmle/code/csharp/PrintAst.qll
index 281e157975ab..fd4bf1cb86b0 100644
--- a/csharp/ql/lib/semmle/code/csharp/PrintAst.qll
+++ b/csharp/ql/lib/semmle/code/csharp/PrintAst.qll
@@ -32,7 +32,9 @@ private predicate shouldPrint(Element e, Location l) {
}
private predicate isImplicitExpression(ControlFlowElement element) {
- element.(Expr).isImplicit() and
+ // Include compiler generated cast expressions and `ToString` calls if
+ // they wrap actual source expressions.
+ element.(Expr).stripImplicit().isImplicit() and
not element instanceof CastExpr and
not element.(OperatorCall).getTarget() instanceof ImplicitConversionOperator and
not element instanceof ElementInitializer
diff --git a/csharp/ql/lib/semmle/code/csharp/commons/Constants.qll b/csharp/ql/lib/semmle/code/csharp/commons/Constants.qll
index 1d6e67aa488d..508ba0e5e87d 100644
--- a/csharp/ql/lib/semmle/code/csharp/commons/Constants.qll
+++ b/csharp/ql/lib/semmle/code/csharp/commons/Constants.qll
@@ -43,7 +43,7 @@ predicate isConstantComparison(ComparisonOperation co, boolean b) {
private module ConstantComparisonOperation {
private import semmle.code.csharp.commons.ComparisonTest
- private SimpleType convertedType(Expr expr) { result = expr.stripImplicitCasts().getType() }
+ private SimpleType convertedType(Expr expr) { result = expr.stripImplicit().getType() }
private int maxValue(Expr expr) {
if convertedType(expr) instanceof IntegralType and exists(expr.getValue())
diff --git a/csharp/ql/lib/semmle/code/csharp/commons/Strings.qll b/csharp/ql/lib/semmle/code/csharp/commons/Strings.qll
index 4e007d61737e..908d1c2fb5ab 100644
--- a/csharp/ql/lib/semmle/code/csharp/commons/Strings.qll
+++ b/csharp/ql/lib/semmle/code/csharp/commons/Strings.qll
@@ -44,11 +44,11 @@ class ImplicitToStringExpr extends Expr {
)
or
exists(AddExpr add, Expr o | o = add.getAnOperand() |
- o.stripImplicitCasts().getType() instanceof StringType and
- this = add.getOtherOperand(o)
+ o.stripImplicit().getType() instanceof StringType and
+ this = add.getOtherOperand(o).stripImplicit()
)
or
- this = any(InterpolatedStringExpr ise).getAnInsert()
+ this = any(InterpolatedStringExpr ise).getAnInsert().stripImplicit()
}
}
diff --git a/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll b/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll
index f72f2dc59a13..a990455f4307 100644
--- a/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll
+++ b/csharp/ql/lib/semmle/code/csharp/dataflow/Nullness.qll
@@ -526,7 +526,11 @@ class Dereference extends G::DereferenceableExpr {
not underlyingType instanceof NullableType
)
) else (
- this = any(QualifiableExpr qe | not qe.isConditional()).getQualifier() and
+ this =
+ any(QualifiableExpr qe |
+ not qe.isConditional() and
+ not qe.(MethodCall).isImplicit()
+ ).getQualifier() and
not this instanceof ThisAccess and
not this instanceof BaseAccess and
not this instanceof TypeAccess
diff --git a/csharp/ql/lib/semmle/code/csharp/dispatch/Dispatch.qll b/csharp/ql/lib/semmle/code/csharp/dispatch/Dispatch.qll
index 080b7d5c7325..7de6c30eb13c 100644
--- a/csharp/ql/lib/semmle/code/csharp/dispatch/Dispatch.qll
+++ b/csharp/ql/lib/semmle/code/csharp/dispatch/Dispatch.qll
@@ -857,7 +857,7 @@ private module Internal {
private predicate hasDynamicArg(int i, Type argumentType) {
exists(Expr argument |
argument = this.getArgument(i) and
- argument.stripImplicitCasts().getType() instanceof DynamicType and
+ argument.stripImplicit().getType() instanceof DynamicType and
argumentType = getAPossibleType(argument, _)
)
}
diff --git a/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll b/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll
index be4577d760eb..eecbc35900aa 100644
--- a/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll
+++ b/csharp/ql/lib/semmle/code/csharp/exprs/Call.qll
@@ -281,6 +281,10 @@ class MethodCall extends Call, QualifiableExpr, LateBindableExpr, @method_invoca
result = this.getArgument(i - 1)
else result = this.getArgument(i)
}
+
+ override Expr stripImplicit() {
+ if this.isImplicit() then result = this.getQualifier().stripImplicit() else result = this
+ }
}
/**
diff --git a/csharp/ql/lib/semmle/code/csharp/exprs/Expr.qll b/csharp/ql/lib/semmle/code/csharp/exprs/Expr.qll
index 935162523a16..85676bbd2701 100644
--- a/csharp/ql/lib/semmle/code/csharp/exprs/Expr.qll
+++ b/csharp/ql/lib/semmle/code/csharp/exprs/Expr.qll
@@ -82,10 +82,18 @@ class Expr extends ControlFlowElement, @expr {
Expr stripCasts() { result = this }
/**
+ * DEPRECATED: Use `stripImplicit` instead.
+ *
* Gets an expression that is the result of stripping (recursively) all
* implicit casts from this expression, if any.
*/
- Expr stripImplicitCasts() { result = this }
+ deprecated Expr stripImplicitCasts() { result = this.stripImplicit() }
+
+ /**
+ * Gets an expression that is the result of stripping (recursively) all
+ * implicit casts and implicit ToString calls from this expression, if any.
+ */
+ Expr stripImplicit() { result = this }
/**
* Gets the explicit parameter name used to pass this expression as an
@@ -714,8 +722,8 @@ class Cast extends Expr {
override Expr stripCasts() { result = this.getExpr().stripCasts() }
- override Expr stripImplicitCasts() {
- if this.isImplicit() then result = this.getExpr().stripImplicitCasts() else result = this
+ override Expr stripImplicit() {
+ if this.isImplicit() then result = this.getExpr().stripImplicit() else result = this
}
}
diff --git a/csharp/ql/src/Bad Practices/VirtualCallInConstructorOrDestructor.ql b/csharp/ql/src/Bad Practices/VirtualCallInConstructorOrDestructor.ql
index 24fccf035b40..d754f1a03c59 100644
--- a/csharp/ql/src/Bad Practices/VirtualCallInConstructorOrDestructor.ql
+++ b/csharp/ql/src/Bad Practices/VirtualCallInConstructorOrDestructor.ql
@@ -34,7 +34,9 @@ predicate overriddenSealed(RefType t, Virtualizable d) {
}
predicate virtualAccessWithThisQualifier(Expr e, Member d) {
- exists(VirtualMethodCall c | c = e and c.getTarget() = d and c.hasThisQualifier())
+ exists(VirtualMethodCall c |
+ c = e and c.getTarget() = d and c.hasThisQualifier() and not c.isImplicit()
+ )
or
exists(VirtualMethodAccess c | c = e and c.getTarget() = d and c.hasThisQualifier())
or
diff --git a/csharp/ql/src/Likely Bugs/Dynamic/BadDynamicCall.ql b/csharp/ql/src/Likely Bugs/Dynamic/BadDynamicCall.ql
index 6044ebbbb5eb..2efac6773f72 100644
--- a/csharp/ql/src/Likely Bugs/Dynamic/BadDynamicCall.ql
+++ b/csharp/ql/src/Likely Bugs/Dynamic/BadDynamicCall.ql
@@ -44,11 +44,11 @@ abstract class BadDynamicCall extends DynamicExpr {
ultimateSsaDef = ssaDef.getAnUltimateDefinition()
|
ultimateSsaDef.getADefinition() =
- any(AssignableDefinition def | source = def.getSource().stripImplicitCasts())
+ any(AssignableDefinition def | source = def.getSource().stripImplicit())
or
ultimateSsaDef.getADefinition() =
any(AssignableDefinitions::ImplicitParameterDefinition p |
- source = p.getParameter().getAnAssignedValue().stripImplicitCasts()
+ source = p.getParameter().getAnAssignedValue().stripImplicit()
)
)
}
diff --git a/csharp/ql/src/Likely Bugs/ObjectComparison.ql b/csharp/ql/src/Likely Bugs/ObjectComparison.ql
index 53b525b6072c..eec1961fbf50 100644
--- a/csharp/ql/src/Likely Bugs/ObjectComparison.ql
+++ b/csharp/ql/src/Likely Bugs/ObjectComparison.ql
@@ -28,7 +28,7 @@ class ReferenceEqualityTestOnObject extends EqualityOperation {
exists(getObjectOperand(this)) and
// Neither operand is 'null'.
not this.getAnOperand() instanceof NullLiteral and
- not exists(Type t | t = this.getAnOperand().stripImplicitCasts().getType() |
+ not exists(Type t | t = this.getAnOperand().stripImplicit().getType() |
t instanceof NullType or
t instanceof ValueType
) and
diff --git a/csharp/ql/test/library-tests/dataflow/global/GetAnOutNode.expected b/csharp/ql/test/library-tests/dataflow/global/GetAnOutNode.expected
index ca13350d527b..643b3e813503 100644
--- a/csharp/ql/test/library-tests/dataflow/global/GetAnOutNode.expected
+++ b/csharp/ql/test/library-tests/dataflow/global/GetAnOutNode.expected
@@ -176,6 +176,7 @@
| GlobalDataFlowStringBuilder.cs:36:21:36:34 | call to method ToString | normal | GlobalDataFlowStringBuilder.cs:36:21:36:34 | call to method ToString |
| GlobalDataFlowStringBuilder.cs:39:19:39:37 | object creation of type StringBuilder | normal | GlobalDataFlowStringBuilder.cs:39:19:39:37 | object creation of type StringBuilder |
| GlobalDataFlowStringBuilder.cs:40:9:40:27 | call to method Append | normal | GlobalDataFlowStringBuilder.cs:40:9:40:27 | call to method Append |
+| GlobalDataFlowStringBuilder.cs:40:23:40:24 | call to method ToString | normal | GlobalDataFlowStringBuilder.cs:40:23:40:24 | call to method ToString |
| GlobalDataFlowStringBuilder.cs:41:21:41:34 | call to method ToString | normal | GlobalDataFlowStringBuilder.cs:41:21:41:34 | call to method ToString |
| GlobalDataFlowStringBuilder.cs:44:9:44:18 | call to method Clear | normal | GlobalDataFlowStringBuilder.cs:44:9:44:18 | call to method Clear |
| GlobalDataFlowStringBuilder.cs:45:23:45:35 | call to method ToString | normal | GlobalDataFlowStringBuilder.cs:45:23:45:35 | call to method ToString |
diff --git a/csharp/ql/test/library-tests/dataflow/global/TaintTrackingPath.expected b/csharp/ql/test/library-tests/dataflow/global/TaintTrackingPath.expected
index f90f71d1ea95..6aa705824b79 100644
--- a/csharp/ql/test/library-tests/dataflow/global/TaintTrackingPath.expected
+++ b/csharp/ql/test/library-tests/dataflow/global/TaintTrackingPath.expected
@@ -514,7 +514,7 @@ edges
| GlobalDataFlowStringBuilder.cs:24:19:24:26 | (...) ... : AppendInterpolatedStringHandler | GlobalDataFlowStringBuilder.cs:24:9:24:10 | [post] access to parameter sb : StringBuilder | provenance | MaD:16 |
| GlobalDataFlowStringBuilder.cs:30:31:30:32 | [post] access to local variable sb : StringBuilder | GlobalDataFlowStringBuilder.cs:31:21:31:22 | access to local variable sb : StringBuilder | provenance | |
| GlobalDataFlowStringBuilder.cs:30:31:30:32 | [post] access to local variable sb : StringBuilder | GlobalDataFlowStringBuilder.cs:35:20:35:21 | access to local variable sb : StringBuilder | provenance | |
-| GlobalDataFlowStringBuilder.cs:30:31:30:32 | [post] access to local variable sb : StringBuilder | GlobalDataFlowStringBuilder.cs:40:20:40:26 | (...) ... : AppendInterpolatedStringHandler | provenance | |
+| GlobalDataFlowStringBuilder.cs:30:31:30:32 | [post] access to local variable sb : StringBuilder | GlobalDataFlowStringBuilder.cs:40:23:40:24 | access to local variable sb : StringBuilder | provenance | |
| GlobalDataFlowStringBuilder.cs:30:35:30:48 | "taint source" : String | GlobalDataFlowStringBuilder.cs:17:64:17:64 | s : String | provenance | |
| GlobalDataFlowStringBuilder.cs:30:35:30:48 | "taint source" : String | GlobalDataFlowStringBuilder.cs:30:31:30:32 | [post] access to local variable sb : StringBuilder | provenance | MaD:14 |
| GlobalDataFlowStringBuilder.cs:31:13:31:17 | access to local variable sink0 : String | GlobalDataFlowStringBuilder.cs:32:15:32:19 | access to local variable sink0 | provenance | |
@@ -527,6 +527,8 @@ edges
| GlobalDataFlowStringBuilder.cs:36:21:36:34 | call to method ToString : String | GlobalDataFlowStringBuilder.cs:36:13:36:17 | access to local variable sink1 : String | provenance | |
| GlobalDataFlowStringBuilder.cs:40:9:40:11 | [post] access to local variable sb2 : StringBuilder | GlobalDataFlowStringBuilder.cs:41:21:41:23 | access to local variable sb2 : StringBuilder | provenance | |
| GlobalDataFlowStringBuilder.cs:40:20:40:26 | (...) ... : AppendInterpolatedStringHandler | GlobalDataFlowStringBuilder.cs:40:9:40:11 | [post] access to local variable sb2 : StringBuilder | provenance | MaD:16 |
+| GlobalDataFlowStringBuilder.cs:40:23:40:24 | access to local variable sb : StringBuilder | GlobalDataFlowStringBuilder.cs:40:23:40:24 | call to method ToString : String | provenance | MaD:17 |
+| GlobalDataFlowStringBuilder.cs:40:23:40:24 | call to method ToString : String | GlobalDataFlowStringBuilder.cs:40:20:40:26 | (...) ... : AppendInterpolatedStringHandler | provenance | |
| GlobalDataFlowStringBuilder.cs:41:13:41:17 | access to local variable sink2 : String | GlobalDataFlowStringBuilder.cs:42:15:42:19 | access to local variable sink2 | provenance | |
| GlobalDataFlowStringBuilder.cs:41:21:41:23 | access to local variable sb2 : StringBuilder | GlobalDataFlowStringBuilder.cs:41:21:41:34 | call to method ToString : String | provenance | MaD:17 |
| GlobalDataFlowStringBuilder.cs:41:21:41:34 | call to method ToString : String | GlobalDataFlowStringBuilder.cs:41:13:41:17 | access to local variable sink2 : String | provenance | |
@@ -1046,6 +1048,8 @@ nodes
| GlobalDataFlowStringBuilder.cs:37:15:37:19 | access to local variable sink1 | semmle.label | access to local variable sink1 |
| GlobalDataFlowStringBuilder.cs:40:9:40:11 | [post] access to local variable sb2 : StringBuilder | semmle.label | [post] access to local variable sb2 : StringBuilder |
| GlobalDataFlowStringBuilder.cs:40:20:40:26 | (...) ... : AppendInterpolatedStringHandler | semmle.label | (...) ... : AppendInterpolatedStringHandler |
+| GlobalDataFlowStringBuilder.cs:40:23:40:24 | access to local variable sb : StringBuilder | semmle.label | access to local variable sb : StringBuilder |
+| GlobalDataFlowStringBuilder.cs:40:23:40:24 | call to method ToString : String | semmle.label | call to method ToString : String |
| GlobalDataFlowStringBuilder.cs:41:13:41:17 | access to local variable sink2 : String | semmle.label | access to local variable sink2 : String |
| GlobalDataFlowStringBuilder.cs:41:21:41:23 | access to local variable sb2 : StringBuilder | semmle.label | access to local variable sb2 : StringBuilder |
| GlobalDataFlowStringBuilder.cs:41:21:41:34 | call to method ToString : String | semmle.label | call to method ToString : String |
diff --git a/csharp/ql/test/library-tests/dataflow/implicittostring/PrintAst.expected b/csharp/ql/test/library-tests/dataflow/implicittostring/PrintAst.expected
new file mode 100644
index 000000000000..bf2a515a8895
--- /dev/null
+++ b/csharp/ql/test/library-tests/dataflow/implicittostring/PrintAst.expected
@@ -0,0 +1,95 @@
+implicitToString.cs:
+# 3| [Class] TestClass
+# 5| 5: [Class] MyClass
+# 5| 4: [InstanceConstructor,PrimaryConstructor] MyClass
+# 7| 5: [Method] ToString
+# 7| -1: [TypeMention] string
+# 8| 4: [BlockStmt] {...}
+# 9| 0: [ReturnStmt] return ...;
+# 9| 0: [StringLiteralUtf16] "tainted"
+# 13| 6: [Method] Sink
+# 13| -1: [TypeMention] Void
+#-----| 2: (Parameters)
+# 13| 0: [Parameter] o
+# 13| -1: [TypeMention] object
+# 13| 4: [BlockStmt] {...}
+# 15| 7: [Method] M1
+# 15| -1: [TypeMention] Void
+# 16| 4: [BlockStmt] {...}
+# 17| 0: [LocalVariableDeclStmt] ... ...;
+# 17| 0: [LocalVariableDeclAndInitExpr] MyClass x1 = ...
+# 17| -1: [TypeMention] MyClass
+# 17| 0: [LocalVariableAccess] access to local variable x1
+# 17| 1: [ObjectCreation] object creation of type MyClass
+# 17| 0: [TypeMention] MyClass
+# 18| 1: [LocalVariableDeclStmt] ... ...;
+# 18| 0: [LocalVariableDeclAndInitExpr] String x2 = ...
+# 18| -1: [TypeMention] string
+# 18| 0: [LocalVariableAccess] access to local variable x2
+# 18| 1: [AddExpr] ... + ...
+# 18| 0: [StringLiteralUtf16] "Hello"
+# 18| 1: [MethodCall] call to method ToString
+# 18| -1: [LocalVariableAccess] access to local variable x1
+# 19| 2: [ExprStmt] ...;
+# 19| 0: [MethodCall] call to method Sink
+# 19| 0: [LocalVariableAccess] access to local variable x2
+# 22| 8: [Method] M2
+# 22| -1: [TypeMention] Void
+# 23| 4: [BlockStmt] {...}
+# 24| 0: [LocalVariableDeclStmt] ... ...;
+# 24| 0: [LocalVariableDeclAndInitExpr] MyClass x1 = ...
+# 24| -1: [TypeMention] MyClass
+# 24| 0: [LocalVariableAccess] access to local variable x1
+# 24| 1: [ObjectCreation] object creation of type MyClass
+# 24| 0: [TypeMention] MyClass
+# 25| 1: [LocalVariableDeclStmt] ... ...;
+# 25| 0: [LocalVariableDeclAndInitExpr] String x2 = ...
+# 25| -1: [TypeMention] string
+# 25| 0: [LocalVariableAccess] access to local variable x2
+# 25| 1: [AddExpr] ... + ...
+# 25| 0: [StringLiteralUtf16] "Hello"
+# 25| 1: [MethodCall] call to method ToString
+# 25| -1: [LocalVariableAccess] access to local variable x1
+# 26| 2: [ExprStmt] ...;
+# 26| 0: [MethodCall] call to method Sink
+# 26| 0: [LocalVariableAccess] access to local variable x2
+# 29| 9: [Method] M3
+# 29| -1: [TypeMention] Void
+# 30| 4: [BlockStmt] {...}
+# 31| 0: [LocalVariableDeclStmt] ... ...;
+# 31| 0: [LocalVariableDeclAndInitExpr] MyClass x1 = ...
+# 31| -1: [TypeMention] MyClass
+# 31| 0: [LocalVariableAccess] access to local variable x1
+# 31| 1: [ObjectCreation] object creation of type MyClass
+# 31| 0: [TypeMention] MyClass
+# 32| 1: [LocalVariableDeclStmt] ... ...;
+# 32| 0: [LocalVariableDeclAndInitExpr] String x2 = ...
+# 32| -1: [TypeMention] string
+# 32| 0: [LocalVariableAccess] access to local variable x2
+# 32| 1: [InterpolatedStringExpr] $"..."
+# 32| 0: [StringLiteralUtf16] "Hello "
+# 32| 1: [MethodCall] call to method ToString
+# 32| -1: [LocalVariableAccess] access to local variable x1
+# 33| 2: [ExprStmt] ...;
+# 33| 0: [MethodCall] call to method Sink
+# 33| 0: [LocalVariableAccess] access to local variable x2
+# 36| 10: [Method] M4
+# 36| -1: [TypeMention] Void
+# 37| 4: [BlockStmt] {...}
+# 38| 0: [LocalVariableDeclStmt] ... ...;
+# 38| 0: [LocalVariableDeclAndInitExpr] MyClass x1 = ...
+# 38| -1: [TypeMention] MyClass
+# 38| 0: [LocalVariableAccess] access to local variable x1
+# 38| 1: [ObjectCreation] object creation of type MyClass
+# 38| 0: [TypeMention] MyClass
+# 39| 1: [LocalVariableDeclStmt] ... ...;
+# 39| 0: [LocalVariableDeclAndInitExpr] String x2 = ...
+# 39| -1: [TypeMention] string
+# 39| 0: [LocalVariableAccess] access to local variable x2
+# 39| 1: [InterpolatedStringExpr] $"..."
+# 39| 0: [StringLiteralUtf16] "Hello "
+# 39| 1: [MethodCall] call to method ToString
+# 39| -1: [LocalVariableAccess] access to local variable x1
+# 40| 2: [ExprStmt] ...;
+# 40| 0: [MethodCall] call to method Sink
+# 40| 0: [LocalVariableAccess] access to local variable x2
diff --git a/csharp/ql/test/library-tests/dataflow/implicittostring/PrintAst.qlref b/csharp/ql/test/library-tests/dataflow/implicittostring/PrintAst.qlref
new file mode 100644
index 000000000000..f867dd01f9f8
--- /dev/null
+++ b/csharp/ql/test/library-tests/dataflow/implicittostring/PrintAst.qlref
@@ -0,0 +1 @@
+shared/PrintAst.ql
\ No newline at end of file
diff --git a/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.cs b/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.cs
new file mode 100644
index 000000000000..84c8737204d8
--- /dev/null
+++ b/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.cs
@@ -0,0 +1,43 @@
+using System;
+
+public class TestClass
+{
+ public class MyClass()
+ {
+ public override string ToString()
+ {
+ return "tainted";
+ }
+ }
+
+ public static void Sink(object o) { }
+
+ public void M1()
+ {
+ var x1 = new MyClass();
+ var x2 = "Hello" + x1.ToString();
+ Sink(x2);
+ }
+
+ public void M2()
+ {
+ var x1 = new MyClass();
+ var x2 = "Hello" + x1;
+ Sink(x2);
+ }
+
+ public void M3()
+ {
+ var x1 = new MyClass();
+ var x2 = $"Hello {x1.ToString()}";
+ Sink(x2);
+ }
+
+ public void M4()
+ {
+ var x1 = new MyClass();
+ var x2 = $"Hello {x1}";
+ Sink(x2);
+ }
+
+}
diff --git a/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.expected b/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.expected
new file mode 100644
index 000000000000..aa77f131e890
--- /dev/null
+++ b/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.expected
@@ -0,0 +1,34 @@
+models
+edges
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:18:28:18:40 | call to method ToString : String | provenance | |
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:25:28:25:29 | call to method ToString : String | provenance | |
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:32:27:32:39 | call to method ToString : String | provenance | |
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:39:27:39:28 | call to method ToString : String | provenance | |
+| implicitToString.cs:18:13:18:14 | access to local variable x2 : String | implicitToString.cs:19:14:19:15 | access to local variable x2 | provenance | |
+| implicitToString.cs:18:28:18:40 | call to method ToString : String | implicitToString.cs:18:13:18:14 | access to local variable x2 : String | provenance | |
+| implicitToString.cs:25:13:25:14 | access to local variable x2 : String | implicitToString.cs:26:14:26:15 | access to local variable x2 | provenance | |
+| implicitToString.cs:25:28:25:29 | call to method ToString : String | implicitToString.cs:25:13:25:14 | access to local variable x2 : String | provenance | |
+| implicitToString.cs:32:13:32:14 | access to local variable x2 : String | implicitToString.cs:33:14:33:15 | access to local variable x2 | provenance | |
+| implicitToString.cs:32:27:32:39 | call to method ToString : String | implicitToString.cs:32:13:32:14 | access to local variable x2 : String | provenance | |
+| implicitToString.cs:39:13:39:14 | access to local variable x2 : String | implicitToString.cs:40:14:40:15 | access to local variable x2 | provenance | |
+| implicitToString.cs:39:27:39:28 | call to method ToString : String | implicitToString.cs:39:13:39:14 | access to local variable x2 : String | provenance | |
+nodes
+| implicitToString.cs:9:20:9:28 | "tainted" : String | semmle.label | "tainted" : String |
+| implicitToString.cs:18:13:18:14 | access to local variable x2 : String | semmle.label | access to local variable x2 : String |
+| implicitToString.cs:18:28:18:40 | call to method ToString : String | semmle.label | call to method ToString : String |
+| implicitToString.cs:19:14:19:15 | access to local variable x2 | semmle.label | access to local variable x2 |
+| implicitToString.cs:25:13:25:14 | access to local variable x2 : String | semmle.label | access to local variable x2 : String |
+| implicitToString.cs:25:28:25:29 | call to method ToString : String | semmle.label | call to method ToString : String |
+| implicitToString.cs:26:14:26:15 | access to local variable x2 | semmle.label | access to local variable x2 |
+| implicitToString.cs:32:13:32:14 | access to local variable x2 : String | semmle.label | access to local variable x2 : String |
+| implicitToString.cs:32:27:32:39 | call to method ToString : String | semmle.label | call to method ToString : String |
+| implicitToString.cs:33:14:33:15 | access to local variable x2 | semmle.label | access to local variable x2 |
+| implicitToString.cs:39:13:39:14 | access to local variable x2 : String | semmle.label | access to local variable x2 : String |
+| implicitToString.cs:39:27:39:28 | call to method ToString : String | semmle.label | call to method ToString : String |
+| implicitToString.cs:40:14:40:15 | access to local variable x2 | semmle.label | access to local variable x2 |
+subpaths
+#select
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:19:14:19:15 | access to local variable x2 | $@ | implicitToString.cs:19:14:19:15 | access to local variable x2 | access to local variable x2 |
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:26:14:26:15 | access to local variable x2 | $@ | implicitToString.cs:26:14:26:15 | access to local variable x2 | access to local variable x2 |
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:33:14:33:15 | access to local variable x2 | $@ | implicitToString.cs:33:14:33:15 | access to local variable x2 | access to local variable x2 |
+| implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:9:20:9:28 | "tainted" : String | implicitToString.cs:40:14:40:15 | access to local variable x2 | $@ | implicitToString.cs:40:14:40:15 | access to local variable x2 | access to local variable x2 |
diff --git a/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.ql b/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.ql
new file mode 100644
index 000000000000..85797414ee5d
--- /dev/null
+++ b/csharp/ql/test/library-tests/dataflow/implicittostring/implicitToString.ql
@@ -0,0 +1,19 @@
+import csharp
+import utils.test.ProvenancePathGraph::ShowProvenance
+
+module TtConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node src) { src.asExpr().(StringLiteral).getValue() = "tainted" }
+
+ predicate isSink(DataFlow::Node sink) {
+ exists(MethodCall mc |
+ mc.getTarget().hasUndecoratedName("Sink") and
+ mc.getAnArgument() = sink.asExpr()
+ )
+ }
+}
+
+module Tt = TaintTracking::Global;
+
+from Tt::PathNode source, Tt::PathNode sink
+where Tt::flowPath(source, sink)
+select source, source, sink, "$@", sink, sink.toString()
diff --git a/csharp/ql/test/library-tests/dataflow/library/FlowSummaries.expected b/csharp/ql/test/library-tests/dataflow/library/FlowSummaries.expected
index 6c5524bfd2d9..50d4b3562337 100644
--- a/csharp/ql/test/library-tests/dataflow/library/FlowSummaries.expected
+++ b/csharp/ql/test/library-tests/dataflow/library/FlowSummaries.expected
@@ -1258,6 +1258,7 @@ summary
| Microsoft.AspNetCore.Http;HttpResponse;OnStarting;(System.Func);Argument[0];Argument[0].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;IEndpointFilter;InvokeAsync;(Microsoft.AspNetCore.Http.EndpointFilterInvocationContext,Microsoft.AspNetCore.Http.EndpointFilterDelegate);Argument[1];Argument[1].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;IMiddleware;InvokeAsync;(Microsoft.AspNetCore.Http.HttpContext,Microsoft.AspNetCore.Http.RequestDelegate);Argument[1];Argument[1].Parameter[delegate-self];value;hq-generated |
+| Microsoft.AspNetCore.Http;PathString;ToString;();Argument[this];ReturnValue;taint;manual |
| Microsoft.AspNetCore.Http;ProblemDetailsOptions;set_CustomizeProblemDetails;(System.Action);Argument[0];Argument[0].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;QueryCollection+Enumerator;get_Current;();Argument[this];ReturnValue;taint;df-generated |
| Microsoft.AspNetCore.Http;QueryCollection;GetEnumerator;();Argument[this].Element;ReturnValue.Property[System.Collections.Generic.IEnumerator`1.Current];value;manual |
diff --git a/csharp/ql/test/library-tests/dataflow/library/FlowSummariesFiltered.expected b/csharp/ql/test/library-tests/dataflow/library/FlowSummariesFiltered.expected
index f6fe3b940435..f99f76e9273b 100644
--- a/csharp/ql/test/library-tests/dataflow/library/FlowSummariesFiltered.expected
+++ b/csharp/ql/test/library-tests/dataflow/library/FlowSummariesFiltered.expected
@@ -415,6 +415,7 @@
| Microsoft.AspNetCore.Http;HttpResponse;OnStarting;(System.Func);Argument[0];Argument[0].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;IEndpointFilter;InvokeAsync;(Microsoft.AspNetCore.Http.EndpointFilterInvocationContext,Microsoft.AspNetCore.Http.EndpointFilterDelegate);Argument[1];Argument[1].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;IMiddleware;InvokeAsync;(Microsoft.AspNetCore.Http.HttpContext,Microsoft.AspNetCore.Http.RequestDelegate);Argument[1];Argument[1].Parameter[delegate-self];value;hq-generated |
+| Microsoft.AspNetCore.Http;PathString;ToString;();Argument[this];ReturnValue;taint;manual |
| Microsoft.AspNetCore.Http;ProblemDetailsOptions;set_CustomizeProblemDetails;(System.Action);Argument[0];Argument[0].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;RequestDelegate;BeginInvoke;(Microsoft.AspNetCore.Http.HttpContext,System.AsyncCallback,System.Object);Argument[1];Argument[1].Parameter[delegate-self];value;hq-generated |
| Microsoft.AspNetCore.Http;RequestDelegateFactory;Create;(System.Reflection.MethodInfo,System.Func,Microsoft.AspNetCore.Http.RequestDelegateFactoryOptions);Argument[1];Argument[1].Parameter[delegate-self];value;hq-generated |
diff --git a/csharp/ql/test/library-tests/implicittostring/implicitToString.cs b/csharp/ql/test/library-tests/implicittostring/implicitToString.cs
new file mode 100644
index 000000000000..d75044246f8e
--- /dev/null
+++ b/csharp/ql/test/library-tests/implicittostring/implicitToString.cs
@@ -0,0 +1,44 @@
+using System;
+
+public class TestImplicitToString
+{
+ public class Container
+ {
+ public override string ToString()
+ {
+ return "Container";
+ }
+ }
+
+ public class FormattableContainer : IFormattable
+ {
+ public string ToString(string format, IFormatProvider formatProvider)
+ {
+ return "Formatted container";
+ }
+
+
+ public override string ToString()
+ {
+ return "Container";
+ }
+ }
+
+ public void M()
+ {
+ var container = new Container();
+
+ var y = "Hello" + container; // Implicit ToString call
+ y = "Hello" + container.ToString();
+ y = $"Hello {container}"; // Implicit ToString() call
+ y = $"Hello {container.ToString()}";
+ y = $"Hello {container:D}"; // Implicit ToString() call.
+
+ var z = "Hello" + y; // No implicit ToString call as `y` is already a string.
+
+ var formattableContainer = new FormattableContainer();
+ y = "Hello" + formattableContainer; // Implicit call to ToString().
+ y = $"Hello {formattableContainer}"; // Implicit call to ToString(string, IFormatProvider). We don't handle this.
+ y = $"Hello {formattableContainer:D}"; // Implicit call to ToString(string, IFormatProvider). We don't handle this.
+ }
+}
diff --git a/csharp/ql/test/library-tests/implicittostring/implicitToString.expected b/csharp/ql/test/library-tests/implicittostring/implicitToString.expected
new file mode 100644
index 000000000000..4878b12a4fdb
--- /dev/null
+++ b/csharp/ql/test/library-tests/implicittostring/implicitToString.expected
@@ -0,0 +1,4 @@
+| implicitToString.cs:31:27:31:35 | call to method ToString |
+| implicitToString.cs:33:22:33:30 | call to method ToString |
+| implicitToString.cs:35:22:35:30 | call to method ToString |
+| implicitToString.cs:40:23:40:42 | call to method ToString |
diff --git a/csharp/ql/test/library-tests/implicittostring/implicitToString.ql b/csharp/ql/test/library-tests/implicittostring/implicitToString.ql
new file mode 100644
index 000000000000..4eb518d84c39
--- /dev/null
+++ b/csharp/ql/test/library-tests/implicittostring/implicitToString.ql
@@ -0,0 +1,5 @@
+import csharp
+
+from MethodCall c
+where c.isImplicit()
+select c
diff --git a/csharp/ql/test/library-tests/security/dataflow/flowsources/StoredFlowSources.expected b/csharp/ql/test/library-tests/security/dataflow/flowsources/StoredFlowSources.expected
index 28156f12272e..e27ea53adbd1 100644
--- a/csharp/ql/test/library-tests/security/dataflow/flowsources/StoredFlowSources.expected
+++ b/csharp/ql/test/library-tests/security/dataflow/flowsources/StoredFlowSources.expected
@@ -6,6 +6,7 @@
| data.cs:28:35:28:71 | ... + ... |
| data.cs:28:51:28:64 | access to local variable customerReader |
| data.cs:28:51:28:71 | access to indexer |
+| data.cs:28:51:28:71 | call to method ToString |
| data.cs:30:13:30:26 | access to local variable customerReader |
| entity.cs:31:29:31:82 | DbRawSqlQuery blogs = ... |
| entity.cs:31:37:31:82 | call to method SqlQuery |
diff --git a/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.cs b/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.cs
index 95ec516ddbe5..767c4e484a14 100644
--- a/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.cs
+++ b/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.cs
@@ -47,6 +47,7 @@ class C : B
f_nonvirtual(); // GOOD
f_interface(); // GOOD
((I)this).f_interface(); // GOOD
+ var x = $"{this}"; // GOOD
// Method access
Action a;
@@ -70,5 +71,10 @@ class C : B
e_sealed += f_nonvirtual; // GOOD
e_nonvirtual += f_nonvirtual; // GOOD
}
+
+ public override string ToString()
+ {
+ return "C";
+ }
}
}
diff --git a/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.expected b/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.expected
index a2f6281ae4ab..8a97e61bf8cf 100644
--- a/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.expected
+++ b/csharp/ql/test/query-tests/Bad Practices/VirtualCallInConstructorOrDestructor/VirtualCallInConstructorOrDestructor.expected
@@ -1,5 +1,5 @@
| VirtualCallInConstructorOrDestructor.cs:45:13:45:23 | call to method f_virtual | Avoid virtual calls in a constructor or destructor. |
-| VirtualCallInConstructorOrDestructor.cs:53:17:53:25 | access to method f_virtual | Avoid virtual calls in a constructor or destructor. |
-| VirtualCallInConstructorOrDestructor.cs:59:21:59:29 | access to property p_virtual | Avoid virtual calls in a constructor or destructor. |
-| VirtualCallInConstructorOrDestructor.cs:64:17:64:23 | access to indexer | Avoid virtual calls in a constructor or destructor. |
-| VirtualCallInConstructorOrDestructor.cs:69:13:69:21 | access to event e_virtual | Avoid virtual calls in a constructor or destructor. |
+| VirtualCallInConstructorOrDestructor.cs:54:17:54:25 | access to method f_virtual | Avoid virtual calls in a constructor or destructor. |
+| VirtualCallInConstructorOrDestructor.cs:60:21:60:29 | access to property p_virtual | Avoid virtual calls in a constructor or destructor. |
+| VirtualCallInConstructorOrDestructor.cs:65:17:65:23 | access to indexer | Avoid virtual calls in a constructor or destructor. |
+| VirtualCallInConstructorOrDestructor.cs:70:13:70:21 | access to event e_virtual | Avoid virtual calls in a constructor or destructor. |
diff --git a/csharp/ql/test/query-tests/Nullness/Implications.expected b/csharp/ql/test/query-tests/Nullness/Implications.expected
index b9c994d08250..45ffbc1d92b0 100644
--- a/csharp/ql/test/query-tests/Nullness/Implications.expected
+++ b/csharp/ql/test/query-tests/Nullness/Implications.expected
@@ -1451,6 +1451,8 @@
| GuardedString.cs:35:31:35:31 | access to local variable s | null | GuardedString.cs:7:20:7:32 | ... ? ... : ... | null |
| GuardedString.cs:37:31:37:31 | access to local variable s | non-null | GuardedString.cs:7:20:7:32 | ... ? ... : ... | non-null |
| GuardedString.cs:37:31:37:31 | access to local variable s | null | GuardedString.cs:7:20:7:32 | ... ? ... : ... | null |
+| ImplicitToString.cs:8:23:8:23 | access to local variable o | non-null | ImplicitToString.cs:7:20:7:23 | null | non-null |
+| ImplicitToString.cs:8:23:8:23 | access to local variable o | null | ImplicitToString.cs:7:20:7:23 | null | null |
| NullAlwaysBad.cs:9:17:9:25 | ... != ... | false | NullAlwaysBad.cs:9:17:9:17 | access to parameter s | null |
| NullAlwaysBad.cs:9:17:9:25 | ... != ... | true | NullAlwaysBad.cs:9:17:9:17 | access to parameter s | non-null |
| NullAlwaysBad.cs:9:17:9:41 | ... \|\| ... | false | NullAlwaysBad.cs:9:17:9:25 | ... != ... | false |
diff --git a/csharp/ql/test/query-tests/Nullness/ImplicitToString.cs b/csharp/ql/test/query-tests/Nullness/ImplicitToString.cs
new file mode 100644
index 000000000000..304021520c2e
--- /dev/null
+++ b/csharp/ql/test/query-tests/Nullness/ImplicitToString.cs
@@ -0,0 +1,10 @@
+using System;
+
+class ImplicitToStringTest
+{
+ void InterpolatedStringImplicitToString()
+ {
+ object o = null;
+ string s = $"{o}"; // GOOD
+ }
+}
diff --git a/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.cs b/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.cs
index 2f863966f351..981b36002663 100644
--- a/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.cs
+++ b/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.cs
@@ -9,5 +9,8 @@ public void M(object o)
Console.WriteLine($"Hello: {o.ToString()}"); // BAD
Console.WriteLine($"Hello: {o}"); // GOOD
+
+ Console.WriteLine("Hello: " + o.ToString()); // BAD
+ Console.WriteLine("Hello: " + o); // GOOD
}
}
diff --git a/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.expected b/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.expected
index 8021c21df2ae..28775378f049 100644
--- a/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.expected
+++ b/csharp/ql/test/query-tests/Useless Code/RedundantToStringCall/RedundantToStringCall.expected
@@ -1,3 +1,4 @@
| RedundantToStringCall.cs:7:27:7:38 | call to method ToString | Redundant call to 'ToString' on a String object. |
| RedundantToStringCall.cs:10:37:10:48 | call to method ToString | Redundant call to 'ToString' on a String object. |
+| RedundantToStringCall.cs:13:39:13:50 | call to method ToString | Redundant call to 'ToString' on a String object. |
| RedundantToStringCallBad.cs:7:45:7:56 | call to method ToString | Redundant call to 'ToString' on a String object. |