From 119ecff3b7166efcbe09f89ed70b9c9aaa3f43ea Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 18 Dec 2025 11:49:32 +0100 Subject: [PATCH 1/6] C#: Implicit span conversion. --- .../ql/lib/semmle/code/csharp/Conversion.qll | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/csharp/ql/lib/semmle/code/csharp/Conversion.qll b/csharp/ql/lib/semmle/code/csharp/Conversion.qll index 99c58ee51c68..7f943c555ff6 100644 --- a/csharp/ql/lib/semmle/code/csharp/Conversion.qll +++ b/csharp/ql/lib/semmle/code/csharp/Conversion.qll @@ -28,6 +28,7 @@ private module Cached { * * - Identity conversions * - Implicit numeric conversions + * - Implicit span conversions * - Implicit nullable conversions * - Implicit reference conversions * - Boxing conversions @@ -38,6 +39,8 @@ private module Cached { or convNumeric(fromType, toType) or + convSpan(fromType, toType) + or convNullableType(fromType, toType) or convRefTypeNonNull(fromType, toType) @@ -81,6 +84,7 @@ private predicate implicitConversionNonNull(Type fromType, Type toType) { * * - Identity conversions * - Implicit numeric conversions + * - Implicit span conversions * - Implicit nullable conversions * - Implicit reference conversions * - Boxing conversions @@ -491,6 +495,53 @@ private predicate convNumericChar(SimpleType toType) { private predicate convNumericFloat(SimpleType toType) { toType instanceof DoubleType } +private class SpanType extends GenericType { + SpanType() { this.getUnboundGeneric() instanceof SystemSpanStruct } + + Type getElementType() { result = this.getTypeArgument(0) } +} + +private class ReadOnlySpanType extends GenericType { + ReadOnlySpanType() { this.getUnboundGeneric() instanceof SystemReadOnlySpanStruct } + + Type getElementType() { result = this.getTypeArgument(0) } +} + +private class SimpleArrayType extends ArrayType { + SimpleArrayType() { + this.getRank() = 1 and + this.getDimension() = 1 + } +} + +/** + * INTERNAL: Do not use. + * + * Holds if there is an implicit span conversion from `fromType` to `toType`. + * + * 10.2.1: Implicit span conversions (added in C# 14). + * [Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-14.0/first-class-span-types#span-conversions) + */ +predicate convSpan(Type fromType, Type toType) { + fromType.(SimpleArrayType).getElementType() = toType.(SpanType).getElementType() + or + exists(Type fromElementType, Type toElementType | + ( + fromElementType = fromType.(SimpleArrayType).getElementType() or + fromElementType = fromType.(SpanType).getElementType() or + fromElementType = fromType.(ReadOnlySpanType).getElementType() + ) and + toElementType = toType.(ReadOnlySpanType).getElementType() + | + convIdentity(fromElementType, toElementType) + or + convCovariance(fromElementType, toElementType) + ) + or + fromType instanceof SystemStringClass and + toType.(ReadOnlySpanType).getElementType() instanceof CharType +} + /** * INTERNAL: Do not use. * @@ -784,8 +835,8 @@ predicate convConversionOperator(Type fromType, Type toType) { ) } -/** 13.1.3.2: Variance conversion. */ -private predicate convVariance(GenericType fromType, GenericType toType) { +pragma[nomagic] +private predicate convVarianceAux(UnboundGenericType ugt, GenericType fromType, GenericType toType) { // Semantically equivalent with // ```ql // ugt = fromType.getUnboundGeneric() @@ -805,10 +856,23 @@ private predicate convVariance(GenericType fromType, GenericType toType) { // ``` // but performance is improved by explicitly evaluating the `i`th argument // only when all preceding arguments are convertible. - Variance::convVarianceSingle(_, fromType, toType) + Variance::convVarianceSingle(ugt, fromType, toType) or + Variance::convVarianceMultiple(ugt, fromType, toType, ugt.getNumberOfTypeParameters() - 1) +} + +/** 13.1.3.2: Variance conversion. */ +private predicate convVariance(GenericType fromType, GenericType toType) { + convVarianceAux(_, fromType, toType) +} + +/** + * Holds, if `fromType` is covariance convertible to `toType`. + */ +private predicate convCovariance(GenericType fromType, GenericType toType) { exists(UnboundGenericType ugt | - Variance::convVarianceMultiple(ugt, fromType, toType, ugt.getNumberOfTypeParameters() - 1) + convVarianceAux(ugt, fromType, toType) and + forall(TypeParameter tp | tp = ugt.getATypeParameter() | tp.isOut()) ) } From 44c9c58b482a933dbbd960a5ca1120da48d2aea2 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 18 Dec 2025 11:50:44 +0100 Subject: [PATCH 2/6] C#: Add implicit span conversion test case. --- .../library-tests/conversion/span/Span.cs | 54 +++++++++++++++++++ .../conversion/span/span.expected | 16 ++++++ .../library-tests/conversion/span/span.ql | 9 ++++ 3 files changed, 79 insertions(+) create mode 100644 csharp/ql/test/library-tests/conversion/span/Span.cs create mode 100644 csharp/ql/test/library-tests/conversion/span/span.expected create mode 100644 csharp/ql/test/library-tests/conversion/span/span.ql diff --git a/csharp/ql/test/library-tests/conversion/span/Span.cs b/csharp/ql/test/library-tests/conversion/span/Span.cs new file mode 100644 index 000000000000..965396b0e250 --- /dev/null +++ b/csharp/ql/test/library-tests/conversion/span/Span.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +public interface CovariantInterface { } + +public interface InvariantInterface { } + +public interface Interface { } + +public class Base { } + +public class Derived : Base { } + +public class C +{ + public void M() + { + string[] stringArray = []; + string[][] stringArrayArray; + string[,] stringArray2D; + + Span stringSpan = stringArray; // string[] -> Span; + + // Covariant conversions to ReadOnlySpan + // Assignments are included to illustrate that this compiles. + // Only the use of the types matter in terms of test output. + ReadOnlySpan> covariantInterfaceBaseReadOnlySpan; + ReadOnlySpan> covariantInterfaceDerivedReadOnlySpan = default; + Span> covariantInterfaceDerivedSpan = default; + CovariantInterface[] covariantInterfaceDerivedArray = []; + covariantInterfaceBaseReadOnlySpan = covariantInterfaceDerivedReadOnlySpan; // ReadOnlySpan> -> ReadOnlySpan> + covariantInterfaceBaseReadOnlySpan = covariantInterfaceDerivedSpan; // Span> -> ReadOnlySpan> + covariantInterfaceBaseReadOnlySpan = covariantInterfaceDerivedArray; // CovariantInterface[] -> ReadOnlySpan> + + // Identify conversions to ReadOnlySpan + ReadOnlySpan stringReadOnlySpan; + stringReadOnlySpan = stringSpan; // Span -> ReadOnlySpan; + stringReadOnlySpan = stringArray; // string[] -> ReadOnlySpan; + + // Convert string to ReadOnlySpan + string s = ""; + ReadOnlySpan charReadOnlySpan = s; // string -> ReadOnlySpan + + // Use the non-covariant interfaces to show that no conversion is possible. + ReadOnlySpan> invariantInterfaceBaseReadOnlySpan; + ReadOnlySpan> invariantInterfaceDerivedReadOnlySpan; + Span> invariantInterfaceDerivedSpan; + InvariantInterface[] invariantInterfaceDerivedArray; + ReadOnlySpan> interfaceBaseReadOnlySpan; + ReadOnlySpan> interfaceDerivedReadOnlySpan; + Span> interfaceDerivedSpan; + Interface[] interfaceDerivedArray; + } +} diff --git a/csharp/ql/test/library-tests/conversion/span/span.expected b/csharp/ql/test/library-tests/conversion/span/span.expected new file mode 100644 index 000000000000..207fa0d75585 --- /dev/null +++ b/csharp/ql/test/library-tests/conversion/span/span.expected @@ -0,0 +1,16 @@ +| CovariantInterface[] | ReadOnlySpan> | +| CovariantInterface[] | ReadOnlySpan> | +| CovariantInterface[] | Span> | +| Interface[] | ReadOnlySpan> | +| Interface[] | Span> | +| InvariantInterface[] | ReadOnlySpan> | +| InvariantInterface[] | Span> | +| ReadOnlySpan> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | +| Span | ReadOnlySpan | +| String[] | ReadOnlySpan | +| String[] | Span | +| string | ReadOnlySpan | diff --git a/csharp/ql/test/library-tests/conversion/span/span.ql b/csharp/ql/test/library-tests/conversion/span/span.ql new file mode 100644 index 000000000000..634649377840 --- /dev/null +++ b/csharp/ql/test/library-tests/conversion/span/span.ql @@ -0,0 +1,9 @@ +import semmle.code.csharp.Conversion + +private class InterestingType extends Type { + InterestingType() { exists(LocalVariable lv | lv.getType() = this) } +} + +from InterestingType sub, InterestingType sup +where convSpan(sub, sup) and sub != sup +select sub.toStringWithTypes() as s1, sup.toStringWithTypes() as s2 order by s1, s2 From 1817f9cdf74217ce0a314c608a40292259b44552 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 18 Dec 2025 11:55:45 +0100 Subject: [PATCH 3/6] C#: Add change-note. --- .../lib/change-notes/2025-12-18-implicit-span-conversions.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 csharp/ql/lib/change-notes/2025-12-18-implicit-span-conversions.md diff --git a/csharp/ql/lib/change-notes/2025-12-18-implicit-span-conversions.md b/csharp/ql/lib/change-notes/2025-12-18-implicit-span-conversions.md new file mode 100644 index 000000000000..0c2f54d20920 --- /dev/null +++ b/csharp/ql/lib/change-notes/2025-12-18-implicit-span-conversions.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* C# 14: Support for *implicit* span conversions in the QL library. From b686890ba6619ed9d8e8e21fdc3cff1a88d9298f Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Tue, 6 Jan 2026 15:02:24 +0100 Subject: [PATCH 4/6] C#: Address review comments. --- .../ql/lib/semmle/code/csharp/Conversion.qll | 23 +++--------- .../library-tests/conversion/span/Span.cs | 36 +++++++++++++------ .../conversion/span/span.expected | 15 ++++++-- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/csharp/ql/lib/semmle/code/csharp/Conversion.qll b/csharp/ql/lib/semmle/code/csharp/Conversion.qll index 7f943c555ff6..e9222c469a70 100644 --- a/csharp/ql/lib/semmle/code/csharp/Conversion.qll +++ b/csharp/ql/lib/semmle/code/csharp/Conversion.qll @@ -535,7 +535,7 @@ predicate convSpan(Type fromType, Type toType) { | convIdentity(fromElementType, toElementType) or - convCovariance(fromElementType, toElementType) + convVariance(fromElementType, toElementType) ) or fromType instanceof SystemStringClass and @@ -835,8 +835,8 @@ predicate convConversionOperator(Type fromType, Type toType) { ) } -pragma[nomagic] -private predicate convVarianceAux(UnboundGenericType ugt, GenericType fromType, GenericType toType) { +/** 13.1.3.2: Variance conversion. */ +private predicate convVariance(GenericType fromType, GenericType toType) { // Semantically equivalent with // ```ql // ugt = fromType.getUnboundGeneric() @@ -856,23 +856,10 @@ private predicate convVarianceAux(UnboundGenericType ugt, GenericType fromType, // ``` // but performance is improved by explicitly evaluating the `i`th argument // only when all preceding arguments are convertible. - Variance::convVarianceSingle(ugt, fromType, toType) + Variance::convVarianceSingle(_, fromType, toType) or - Variance::convVarianceMultiple(ugt, fromType, toType, ugt.getNumberOfTypeParameters() - 1) -} - -/** 13.1.3.2: Variance conversion. */ -private predicate convVariance(GenericType fromType, GenericType toType) { - convVarianceAux(_, fromType, toType) -} - -/** - * Holds, if `fromType` is covariance convertible to `toType`. - */ -private predicate convCovariance(GenericType fromType, GenericType toType) { exists(UnboundGenericType ugt | - convVarianceAux(ugt, fromType, toType) and - forall(TypeParameter tp | tp = ugt.getATypeParameter() | tp.isOut()) + Variance::convVarianceMultiple(ugt, fromType, toType, ugt.getNumberOfTypeParameters() - 1) ) } diff --git a/csharp/ql/test/library-tests/conversion/span/Span.cs b/csharp/ql/test/library-tests/conversion/span/Span.cs index 965396b0e250..fd2fd3a536f5 100644 --- a/csharp/ql/test/library-tests/conversion/span/Span.cs +++ b/csharp/ql/test/library-tests/conversion/span/Span.cs @@ -3,9 +3,11 @@ public interface CovariantInterface { } +public interface ContravariantInterface { } + public interface InvariantInterface { } -public interface Interface { } +public interface MixedInterface { } public class Base { } @@ -16,14 +18,14 @@ public class C public void M() { string[] stringArray = []; - string[][] stringArrayArray; - string[,] stringArray2D; + string[][] stringArrayArray = []; + string[,] stringArray2D = new string[0, 0]; Span stringSpan = stringArray; // string[] -> Span; - // Covariant conversions to ReadOnlySpan - // Assignments are included to illustrate that this compiles. + // Assignments are included to illustrate that it compiles. // Only the use of the types matter in terms of test output. + // Covariant conversions to ReadOnlySpan ReadOnlySpan> covariantInterfaceBaseReadOnlySpan; ReadOnlySpan> covariantInterfaceDerivedReadOnlySpan = default; Span> covariantInterfaceDerivedSpan = default; @@ -37,18 +39,32 @@ public void M() stringReadOnlySpan = stringSpan; // Span -> ReadOnlySpan; stringReadOnlySpan = stringArray; // string[] -> ReadOnlySpan; + // Contravariant conversions to ReadOnlySpan + ReadOnlySpan> contravariantInterfaceDerivedReadOnlySpan; + ReadOnlySpan> contravariantInterfaceBaseReadOnlySpan = default; + Span> contravariantInterfaceBaseSpan = default; + ContravariantInterface[] contravariantInterfaceBaseArray = []; + contravariantInterfaceDerivedReadOnlySpan = contravariantInterfaceBaseReadOnlySpan; // ReadOnlySpan> -> ReadOnlySpan> + contravariantInterfaceDerivedReadOnlySpan = contravariantInterfaceBaseSpan; // Span> -> ReadOnlySpan> + contravariantInterfaceDerivedReadOnlySpan = contravariantInterfaceBaseArray; // ContravariantInterface[] -> ReadOnlySpan> + + // Mixed variance conversions to ReadOnlySpan + ReadOnlySpan> mixedInterfaceBaseReadOnlySpan; + ReadOnlySpan> mixedInterfaceDerivedReadOnlySpan = default; + Span> mixedInterfaceDerivedSpan = default; + MixedInterface[] mixedInterfaceDerivedArray = []; + mixedInterfaceBaseReadOnlySpan = mixedInterfaceDerivedReadOnlySpan; // ReadOnlySpan> -> ReadOnlySpan> + mixedInterfaceBaseReadOnlySpan = mixedInterfaceDerivedSpan; // Span> -> ReadOnlySpan> + mixedInterfaceBaseReadOnlySpan = mixedInterfaceDerivedArray; // MixedInterface[] -> ReadOnlySpan> + // Convert string to ReadOnlySpan string s = ""; ReadOnlySpan charReadOnlySpan = s; // string -> ReadOnlySpan - // Use the non-covariant interfaces to show that no conversion is possible. + // No conversion possible except for identity. ReadOnlySpan> invariantInterfaceBaseReadOnlySpan; ReadOnlySpan> invariantInterfaceDerivedReadOnlySpan; Span> invariantInterfaceDerivedSpan; InvariantInterface[] invariantInterfaceDerivedArray; - ReadOnlySpan> interfaceBaseReadOnlySpan; - ReadOnlySpan> interfaceDerivedReadOnlySpan; - Span> interfaceDerivedSpan; - Interface[] interfaceDerivedArray; } } diff --git a/csharp/ql/test/library-tests/conversion/span/span.expected b/csharp/ql/test/library-tests/conversion/span/span.expected index 207fa0d75585..5a5a2bc6f39d 100644 --- a/csharp/ql/test/library-tests/conversion/span/span.expected +++ b/csharp/ql/test/library-tests/conversion/span/span.expected @@ -1,15 +1,24 @@ +| ContravariantInterface[] | ReadOnlySpan> | +| ContravariantInterface[] | ReadOnlySpan> | +| ContravariantInterface[] | Span> | | CovariantInterface[] | ReadOnlySpan> | | CovariantInterface[] | ReadOnlySpan> | | CovariantInterface[] | Span> | -| Interface[] | ReadOnlySpan> | -| Interface[] | Span> | | InvariantInterface[] | ReadOnlySpan> | | InvariantInterface[] | Span> | +| MixedInterface[] | ReadOnlySpan> | +| MixedInterface[] | ReadOnlySpan> | +| MixedInterface[] | Span> | +| ReadOnlySpan> | ReadOnlySpan> | | ReadOnlySpan> | ReadOnlySpan> | +| ReadOnlySpan> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | | Span> | ReadOnlySpan> | | Span> | ReadOnlySpan> | -| Span> | ReadOnlySpan> | | Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan> | | Span | ReadOnlySpan | | String[] | ReadOnlySpan | | String[] | Span | From a991afdf697a11f28e8bd2230b3da01897ca03cd Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 7 Jan 2026 09:55:10 +0100 Subject: [PATCH 5/6] C#: Use ref conversions (including variance conversions) for element types of span conversions. --- csharp/ql/lib/semmle/code/csharp/Conversion.qll | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/csharp/ql/lib/semmle/code/csharp/Conversion.qll b/csharp/ql/lib/semmle/code/csharp/Conversion.qll index e9222c469a70..ec7ef9cac952 100644 --- a/csharp/ql/lib/semmle/code/csharp/Conversion.qll +++ b/csharp/ql/lib/semmle/code/csharp/Conversion.qll @@ -533,9 +533,7 @@ predicate convSpan(Type fromType, Type toType) { ) and toElementType = toType.(ReadOnlySpanType).getElementType() | - convIdentity(fromElementType, toElementType) - or - convVariance(fromElementType, toElementType) + convRefTypeNonNull(fromElementType, toElementType) ) or fromType instanceof SystemStringClass and From 8fe31a1fd6a5b487f43e8f4db671acd67dbd5017 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 7 Jan 2026 09:56:02 +0100 Subject: [PATCH 6/6] C#: Add some more testcases and update test expected output. --- .../library-tests/conversion/span/Span.cs | 11 ++++++++++ .../conversion/span/span.expected | 22 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/csharp/ql/test/library-tests/conversion/span/Span.cs b/csharp/ql/test/library-tests/conversion/span/Span.cs index fd2fd3a536f5..ae1ac5b40866 100644 --- a/csharp/ql/test/library-tests/conversion/span/Span.cs +++ b/csharp/ql/test/library-tests/conversion/span/Span.cs @@ -61,6 +61,17 @@ public void M() string s = ""; ReadOnlySpan charReadOnlySpan = s; // string -> ReadOnlySpan + // Various ref type conversions + Derived[] derivedArray = []; + ReadOnlySpan baseReadOnlySpan; + baseReadOnlySpan = derivedArray; // Derived[] -> ReadOnlySpan + + ReadOnlySpan objectReadOnlySpan; + objectReadOnlySpan = stringArray; // string[] -> ReadOnlySpan + + byte[][] byteByteArray = []; + objectReadOnlySpan = byteByteArray; // byte[][] -> ReadOnlySpan + // No conversion possible except for identity. ReadOnlySpan> invariantInterfaceBaseReadOnlySpan; ReadOnlySpan> invariantInterfaceDerivedReadOnlySpan; diff --git a/csharp/ql/test/library-tests/conversion/span/span.expected b/csharp/ql/test/library-tests/conversion/span/span.expected index 5a5a2bc6f39d..1792d46898cd 100644 --- a/csharp/ql/test/library-tests/conversion/span/span.expected +++ b/csharp/ql/test/library-tests/conversion/span/span.expected @@ -1,25 +1,47 @@ | ContravariantInterface[] | ReadOnlySpan> | | ContravariantInterface[] | ReadOnlySpan> | +| ContravariantInterface[] | ReadOnlySpan | | ContravariantInterface[] | Span> | | CovariantInterface[] | ReadOnlySpan> | | CovariantInterface[] | ReadOnlySpan> | +| CovariantInterface[] | ReadOnlySpan | | CovariantInterface[] | Span> | +| Derived[] | ReadOnlySpan | +| Derived[] | ReadOnlySpan | | InvariantInterface[] | ReadOnlySpan> | +| InvariantInterface[] | ReadOnlySpan | | InvariantInterface[] | Span> | | MixedInterface[] | ReadOnlySpan> | | MixedInterface[] | ReadOnlySpan> | +| MixedInterface[] | ReadOnlySpan | | MixedInterface[] | Span> | +| ReadOnlySpan | ReadOnlySpan | | ReadOnlySpan> | ReadOnlySpan> | +| ReadOnlySpan> | ReadOnlySpan | +| ReadOnlySpan> | ReadOnlySpan | +| ReadOnlySpan> | ReadOnlySpan | | ReadOnlySpan> | ReadOnlySpan> | +| ReadOnlySpan> | ReadOnlySpan | +| ReadOnlySpan> | ReadOnlySpan | +| ReadOnlySpan> | ReadOnlySpan | +| ReadOnlySpan> | ReadOnlySpan | | ReadOnlySpan> | ReadOnlySpan> | +| ReadOnlySpan> | ReadOnlySpan | +| ReadOnlySpan | ReadOnlySpan | | Span> | ReadOnlySpan> | | Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan | | Span> | ReadOnlySpan> | | Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan | | Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan | | Span> | ReadOnlySpan> | | Span> | ReadOnlySpan> | +| Span> | ReadOnlySpan | +| Span | ReadOnlySpan | | Span | ReadOnlySpan | +| String[] | ReadOnlySpan | | String[] | ReadOnlySpan | | String[] | Span | | string | ReadOnlySpan |