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..ec7ef9cac952 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,51 @@ 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() + | + convRefTypeNonNull(fromElementType, toElementType) + ) + or + fromType instanceof SystemStringClass and + toType.(ReadOnlySpanType).getElementType() instanceof CharType +} + /** * INTERNAL: Do not use. * 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..ae1ac5b40866 --- /dev/null +++ b/csharp/ql/test/library-tests/conversion/span/Span.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; + +public interface CovariantInterface { } + +public interface ContravariantInterface { } + +public interface InvariantInterface { } + +public interface MixedInterface { } + +public class Base { } + +public class Derived : Base { } + +public class C +{ + public void M() + { + string[] stringArray = []; + string[][] stringArrayArray = []; + string[,] stringArray2D = new string[0, 0]; + + Span stringSpan = stringArray; // string[] -> Span; + + // 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; + 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; + + // 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 + + // 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; + Span> invariantInterfaceDerivedSpan; + InvariantInterface[] invariantInterfaceDerivedArray; + } +} 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..1792d46898cd --- /dev/null +++ b/csharp/ql/test/library-tests/conversion/span/span.expected @@ -0,0 +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 | 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