Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<PackageVersion Include="McMaster.Extensions.Hosting.CommandLine" Version="4.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="5.0.0" />
<PackageVersion Include="Microsoft.DiaSymReader.Converter.Xml" Version="1.1.0-beta2-22171-02" />
<PackageVersion Include="Microsoft.DiaSymReader" Version="1.4.0" />
<PackageVersion Include="Microsoft.DiaSymReader.Native" Version="17.0.0-beta1.21524.1" />
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ internal static DecompilerSettings GetSettings(CompilerOptions cscOptions)
CompilerOptions.UseRoslyn1_3_2 => CSharp.LanguageVersion.CSharp6,
CompilerOptions.UseRoslyn2_10_0 => CSharp.LanguageVersion.CSharp7_3,
CompilerOptions.UseRoslyn3_11_0 => CSharp.LanguageVersion.CSharp9_0,
_ => cscOptions.HasFlag(CompilerOptions.Preview) ? CSharp.LanguageVersion.Latest : CSharp.LanguageVersion.CSharp13_0,
_ => cscOptions.HasFlag(CompilerOptions.Preview) ? CSharp.LanguageVersion.Latest : CSharp.LanguageVersion.CSharp14_0,
};
DecompilerSettings settings = new(langVersion) {
// Never use file-scoped namespaces
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ public async Task Issue3598([ValueSource(nameof(roslyn4OrNewerOptions))] Compile
[Test]
public async Task ExtensionProperties([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview);
await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview | CompilerOptions.NullableEnable);
}

[Test]
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.Decompiler/CSharp/CallBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,7 +1438,7 @@ private ExpressionWithResolveResult HandleImplicitConversion(IMethod method, Tra
var conversions = CSharpConversions.Get(expressionBuilder.compilation);
IType targetType = method.ReturnType;
var conv = conversions.ImplicitConversion(argument.Type, targetType);
if (!(conv.IsUserDefined && conv.IsValid && conv.Method.Equals(method)))
if (!(conv.IsUserDefined && conv.IsValid && conv.Method.Equals(method, NormalizeTypeVisitor.TypeErasure)))
{
// implicit conversion to targetType isn't directly possible, so first insert a cast to the argument type
argument = argument.ConvertTo(method.Parameters[0].Type, expressionBuilder);
Expand Down
174 changes: 159 additions & 15 deletions ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ Conversion StandardImplicitConversion(IType fromType, IType toType, bool allowTu
if (IdentityConversion(elementType, spanElementType))
return Conversion.InlineArrayConversion;
}
if (IsImplicitSpanConversion(fromType, toType))
{
return Conversion.ImplicitSpanConversion;
}
return Conversion.None;
}

Expand Down Expand Up @@ -1229,6 +1233,49 @@ List<OperatorInfo> GetApplicableConversionOperators(ResolveResult fromResult, IT
}
#endregion

#region Implicit Span Conversion

bool IsImplicitSpanConversion(IType fromType, IType toType)
{
if (!compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes))
{
return false;
}

// An implicit span conversion permits array_types, System.Span<T>, System.ReadOnlySpan<T>,
// and string to be converted between each other
// see https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-14.0/first-class-span-types#span-conversions

switch (fromType)
{
case ArrayType { Dimensions: 1, ElementType: var elementType }:
if (toType.IsKnownType(KnownTypeCode.SpanOfT))
{
return IdentityConversion(elementType, toType.TypeArguments[0]);
}
if (toType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
return IdentityConversion(elementType, toType.TypeArguments[0])
|| IsImplicitReferenceConversion(elementType, toType.TypeArguments[0]);
Comment on lines +1258 to +1259
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logical OR operation spans multiple lines without clear visual separation. Consider extracting this complex boolean expression into a well-named local variable or helper method to improve readability, such as bool hasValidConversion = IdentityConversion(...) || IsImplicitReferenceConversion(...);

Suggested change
return IdentityConversion(elementType, toType.TypeArguments[0])
|| IsImplicitReferenceConversion(elementType, toType.TypeArguments[0]);
bool hasValidConversion = IdentityConversion(elementType, toType.TypeArguments[0])
|| IsImplicitReferenceConversion(elementType, toType.TypeArguments[0]);
return hasValidConversion;

Copilot uses AI. Check for mistakes.
}
break;
case ParameterizedType pt when pt.IsKnownType(KnownTypeCode.SpanOfT) || pt.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
if (toType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
return IdentityConversion(pt.TypeArguments[0], toType.TypeArguments[0])
|| IsImplicitReferenceConversion(pt.TypeArguments[0], toType.TypeArguments[0]);
Comment on lines +1265 to +1266
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated logic from lines 1258-1259. Consider extracting this into a helper method like bool HasIdentityOrImplicitReferenceConversion(IType from, IType to) to reduce code duplication.

Copilot uses AI. Check for mistakes.
}
break;
case var s when s.IsKnownType(KnownTypeCode.String):
return toType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& toType.TypeArguments[0].IsKnownType(KnownTypeCode.Char);
}

return false;
}

#endregion

#region AnonymousFunctionConversion
Conversion AnonymousFunctionConversion(ResolveResult resolveResult, IType toType)
{
Expand Down Expand Up @@ -1487,11 +1534,32 @@ Conversion TupleConversion(IType fromType, IType toType, bool isExplicit)

#region BetterConversion
/// <summary>
/// Gets the better conversion (C# 4.0 spec, §7.5.3.3)
/// Gets the better conversion (from expression) (C# 8.0 spec, §12.6.4.5)
/// </summary>
/// <returns>0 = neither is better; 1 = t1 is better; 2 = t2 is better</returns>
public int BetterConversion(ResolveResult resolveResult, IType t1, IType t2)
{
bool t1Exact = IsExactlyMatching(resolveResult, t1);
bool t2Exact = IsExactlyMatching(resolveResult, t2);
if (t1Exact && !t2Exact)
return 1;
if (t2Exact && !t1Exact)
return 2;
if (!t1Exact && !t2Exact)
{
bool c1ImplicitSpanConversion = IsImplicitSpanConversion(resolveResult.Type, t1);
bool c2ImplicitSpanConversion = IsImplicitSpanConversion(resolveResult.Type, t2);
if (c1ImplicitSpanConversion && !c2ImplicitSpanConversion)
return 1;
if (c2ImplicitSpanConversion && !c1ImplicitSpanConversion)
return 2;
}
if (t1Exact == t2Exact)
{
int r = BetterConversionTarget(t1, t2);
if (r != 0)
return r;
}
LambdaResolveResult lambda = resolveResult as LambdaResolveResult;
if (lambda != null)
{
Expand Down Expand Up @@ -1542,20 +1610,56 @@ public int BetterConversion(ResolveResult resolveResult, IType t1, IType t2)
}

/// <summary>
/// Unpacks the generic Task[T]. Returns null if the input is not Task[T].
/// Gets whether an expression E exactly matches a type T (C# 8.0 spec, §12.6.4.6)
/// </summary>
static IType UnpackTask(IType type)
bool IsExactlyMatching(ResolveResult e, IType t)
{
ParameterizedType pt = type as ParameterizedType;
if (pt != null && pt.TypeParameterCount == 1 && pt.Name == "Task" && pt.Namespace == "System.Threading.Tasks")
var s = e.Type;
if (IdentityConversion(s, t))
return true;
if (e is LambdaResolveResult lambda)
{
return pt.GetTypeArgument(0);
if (!lambda.IsAnonymousMethod)
{
t = UnpackExpressionTreeType(t);
}
IMethod m = t.GetDelegateInvokeMethod();
if (m == null)
return false;
IType[] parameterTypes = new IType[m.Parameters.Count];
for (int i = 0; i < parameterTypes.Length; i++)
parameterTypes[i] = m.Parameters[i].Type;
var x = lambda.GetInferredReturnType(parameterTypes);
var y = m.ReturnType;
if (IdentityConversion(x, y))
return true;
if (lambda.IsAsync)
{
x = UnpackTask(x);
y = UnpackTask(y);
}
if (x != null && y != null)
return IsExactlyMatching(new ResolveResult(x), y);
return false;
}
else
{
return false;
}
return null;
}

/// <summary>
/// Gets the better conversion (C# 4.0 spec, §7.5.3.4)
/// Unpacks the generic TaskType[T]. Returns null if the input is not TaskType[T].
/// </summary>
static IType UnpackTask(IType type)
{
return (TaskType.IsTask(type) || TaskType.IsCustomTask(type, out _)) && type.TypeParameterCount == 1
? type.TypeArguments[0]
: null;
}

/// <summary>
/// Gets the better conversion (from type) (C# 4.0 spec, §7.5.3.4)
/// </summary>
/// <returns>0 = neither is better; 1 = t1 is better; 2 = t2 is better</returns>
public int BetterConversion(IType s, IType t1, IType t2)
Expand All @@ -1570,17 +1674,57 @@ public int BetterConversion(IType s, IType t1, IType t2)
}

/// <summary>
/// Gets the better conversion target (C# 4.0 spec, §7.5.3.5)
/// Gets the better conversion target (C# 9.0 spec, §12.6.4.7)
/// </summary>
/// <returns>0 = neither is better; 1 = t1 is better; 2 = t2 is better</returns>
int BetterConversionTarget(IType t1, IType t2)
{
bool t1To2 = ImplicitConversion(t1, t2).IsValid;
bool t2To1 = ImplicitConversion(t2, t1).IsValid;
if (t1To2 && !t2To1)
return 1;
if (t2To1 && !t1To2)
return 2;
if (t1.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
if (t2.IsKnownType(KnownTypeCode.SpanOfT))
{
if (IdentityConversion(t1.TypeArguments[0], t2.TypeArguments[0]))
return 1;
}
if (t2.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
bool t1To2 = ImplicitConversion(t1.TypeArguments[0], t2.TypeArguments[0]).IsValid;
bool t2To1 = ImplicitConversion(t2.TypeArguments[0], t1.TypeArguments[0]).IsValid;
if (t1To2 && !t2To1)
return 1;
}
}

if (t2.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
if (t1.IsKnownType(KnownTypeCode.SpanOfT))
{
if (IdentityConversion(t2.TypeArguments[0], t1.TypeArguments[0]))
return 2;
}
if (t1.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
bool t1To2 = ImplicitConversion(t1.TypeArguments[0], t2.TypeArguments[0]).IsValid;
bool t2To1 = ImplicitConversion(t2.TypeArguments[0], t1.TypeArguments[0]).IsValid;
if (t2To1 && !t1To2)
return 2;
}
}

{
bool t1To2 = ImplicitConversion(t1, t2).IsValid;
bool t2To1 = ImplicitConversion(t2, t1).IsValid;
if (t1To2 && !t2To1)
return 1;
if (t2To1 && !t1To2)
return 2;
}

var s1 = UnpackTask(t1);
var s2 = UnpackTask(t2);
if (s1 != null && s2 != null)
return BetterConversionTarget(s1, s2);

TypeCode t1Code = ReflectionHelper.GetTypeCode(t1);
TypeCode t2Code = ReflectionHelper.GetTypeCode(t2);
if (IsBetterIntegralType(t1Code, t2Code))
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2171,7 +2171,7 @@ static bool IsEligibleExtensionMethod(ICompilation compilation, CSharpConversion
thisParameterType = thisParameterType.AcceptVisitor(substitution);
}
Conversion c = conversions.ImplicitConversion(targetType, thisParameterType);
return c.IsValid && (c.IsIdentityConversion || c.IsReferenceConversion || c.IsBoxingConversion);
return c.IsValid && (c.IsIdentityConversion || c.IsReferenceConversion || c.IsBoxingConversion || c.IsImplicitSpanConversion);
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -710,8 +710,8 @@ void CheckApplicability(Candidate candidate)
candidate.ArgumentConversions[i] = c;
if (IsExtensionMethodInvocation && parameterIndex == 0)
{
// First parameter to extension method must be an identity, reference or boxing conversion
if (!(c == Conversion.IdentityConversion || c == Conversion.ImplicitReferenceConversion || c == Conversion.BoxingConversion))
// First parameter to extension method must be an identity, reference, boxing or span conversion
if (!(c == Conversion.IdentityConversion || c == Conversion.ImplicitReferenceConversion || c == Conversion.BoxingConversion || c == Conversion.ImplicitSpanConversion))
candidate.AddError(OverloadResolutionErrors.ArgumentTypeMismatch);
}
else
Expand Down
66 changes: 46 additions & 20 deletions ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -659,17 +659,31 @@ void MakeExactInference(IType U, IType V)
return;
}
// Handle array types:
ArrayType arrU = U as ArrayType;
ArrayType arrV = V as ArrayType;
if (arrU != null && arrV != null && arrU.Dimensions == arrV.Dimensions)
U = U.TupleUnderlyingTypeOrSelf();
V = V.TupleUnderlyingTypeOrSelf();
switch ((U, V))
{
MakeExactInference(arrU.ElementType, arrV.ElementType);
return;
case (ArrayType arrU, ArrayType arrV) when arrU.Dimensions == arrV.Dimensions:
MakeExactInference(arrU.ElementType, arrV.ElementType);
return;
case (ArrayType arrU, ParameterizedType spanV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanV.IsKnownType(KnownTypeCode.SpanOfT):
MakeExactInference(arrU.ElementType, spanV.TypeArguments[0]);
return;
case (ParameterizedType spanU, ParameterizedType spanV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanU.IsKnownType(KnownTypeCode.SpanOfT) && spanV.IsKnownType(KnownTypeCode.SpanOfT):
MakeExactInference(spanU.TypeArguments[0], spanV.TypeArguments[0]);
return;
case (ArrayType arrU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeExactInference(arrU.ElementType, rosV.TypeArguments[0]);
return;
case (ParameterizedType spanU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanU.IsKnownType(KnownTypeCode.SpanOfT) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeExactInference(spanU.TypeArguments[0], rosV.TypeArguments[0]);
return;
case (ParameterizedType rosU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && rosU.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeExactInference(rosU.TypeArguments[0], rosV.TypeArguments[0]);
return;
}
// Handle parameterized type:
ParameterizedType pU = U.TupleUnderlyingTypeOrSelf() as ParameterizedType;
ParameterizedType pV = V.TupleUnderlyingTypeOrSelf() as ParameterizedType;
if (pU != null && pV != null
if (U is ParameterizedType pU && V is ParameterizedType pV
&& object.Equals(pU.GenericType, pV.GenericType)
&& pU.TypeParameterCount == pV.TypeParameterCount)
{
Expand Down Expand Up @@ -751,21 +765,33 @@ void MakeLowerBoundInference(IType U, IType V)
return;
}
// Handle array types:
ArrayType arrU = U as ArrayType;
ArrayType arrV = V as ArrayType;
ParameterizedType pV = V.TupleUnderlyingTypeOrSelf() as ParameterizedType;
if (arrU != null && arrV != null && arrU.Dimensions == arrV.Dimensions)
V = V.TupleUnderlyingTypeOrSelf();
switch ((U, V))
{
MakeLowerBoundInference(arrU.ElementType, arrV.ElementType);
return;
}
else if (arrU != null && pV.IsArrayInterfaceType() && arrU.Dimensions == 1)
{
MakeLowerBoundInference(arrU.ElementType, pV.GetTypeArgument(0));
return;
case (ArrayType arrU, ArrayType arrV) when arrU.Dimensions == arrV.Dimensions:
MakeLowerBoundInference(arrU.ElementType, arrV.ElementType);
return;
case (ArrayType arrU, ParameterizedType spanV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanV.IsKnownType(KnownTypeCode.SpanOfT):
MakeLowerBoundInference(arrU.ElementType, spanV.TypeArguments[0]);
return;
case (ParameterizedType spanU, ParameterizedType spanV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanU.IsKnownType(KnownTypeCode.SpanOfT) && spanV.IsKnownType(KnownTypeCode.SpanOfT):
MakeLowerBoundInference(spanU.TypeArguments[0], spanV.TypeArguments[0]);
return;
case (ArrayType arrU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeLowerBoundInference(arrU.ElementType, rosV.TypeArguments[0]);
return;
case (ParameterizedType spanU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanU.IsKnownType(KnownTypeCode.SpanOfT) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeLowerBoundInference(spanU.TypeArguments[0], rosV.TypeArguments[0]);
return;
case (ParameterizedType rosU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && rosU.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeLowerBoundInference(rosU.TypeArguments[0], rosV.TypeArguments[0]);
return;
case (ArrayType arrU, ParameterizedType arrIntfV) when arrIntfV.IsArrayInterfaceType() && arrU.Dimensions == 1:
MakeLowerBoundInference(arrU.ElementType, arrIntfV.TypeArguments[0]);
return;
}
// Handle parameterized types:
if (pV != null)
if (V is ParameterizedType pV)
{
ParameterizedType uniqueBaseType = null;
foreach (IType baseU in U.GetAllBaseTypes())
Expand Down
Loading
Loading