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. 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()) ) } 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