diff --git a/.github/workflows/build-ilspy.yml b/.github/workflows/build-ilspy.yml index 03bc37cc4..8ce3864f3 100644 --- a/.github/workflows/build-ilspy.yml +++ b/.github/workflows/build-ilspy.yml @@ -44,7 +44,10 @@ jobs: uses: microsoft/setup-msbuild@v2 - name: Install dotnet-format - run: dotnet tool install -g dotnet-format --version "9.0.520307" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json + env: + DOTNET_FORMAT_VERSION: 10.0.100-preview.6.25358.103 + DOTNET_FORMAT_SOURCE: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10-transport/nuget/v3/index.json + run: dotnet tool install -g dotnet-format --version "${{env.DOTNET_FORMAT_VERSION}}" --add-source "${{env.DOTNET_FORMAT_SOURCE}}" - name: Install wix (locked version) run: dotnet tool install --global wix --version 6.0.0 diff --git a/BuildTools/pre-commit b/BuildTools/pre-commit index 8d455651a..dcaa14e0d 100644 --- a/BuildTools/pre-commit +++ b/BuildTools/pre-commit @@ -5,11 +5,12 @@ set -eu -DOTNET_FORMAT_VERSION=9.0.520307 +DOTNET_FORMAT_VERSION=10.0.100-preview.6.25358.103 +DOTNET_FORMAT_SOURCE="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10-transport/nuget/v3/index.json" DOTNET_PATH="$LOCALAPPDATA/ICSharpCode/ILSpy/dotnet-format-$DOTNET_FORMAT_VERSION" if [ ! -d "$DOTNET_PATH" ]; then echo "Downloading dotnet-format $DOTNET_FORMAT_VERSION..." - dotnet tool install --tool-path "$DOTNET_PATH" dotnet-format --version "$DOTNET_FORMAT_VERSION" --add-source "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" + dotnet tool install --tool-path "$DOTNET_PATH" dotnet-format --version "$DOTNET_FORMAT_VERSION" --add-source "$DOTNET_FORMAT_SOURCE" fi "$DOTNET_PATH/dotnet-format.exe" --version diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 1993cc873..0282f9d00 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -8,7 +8,7 @@ net10.0-windows - 13 + preview win-x64 win-arm64 @@ -146,6 +146,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index dfc3eb8ac..d67877e72 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -527,6 +527,12 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions | CompilerOptions.NullableEnable); } + [Test] + public async Task ExtensionProperties([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); + } + [Test] public async Task NullPropagation([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionProperties.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionProperties.cs new file mode 100644 index 000000000..691276561 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionProperties.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal static class ExtensionProperties + { + extension(ICollection collection) where T : notnull + { + public bool IsEmpty => collection.Count == 0; + + public int Test { + get { + return 42; + } + set { + } + } + + public void AddIfNotNull(T item) + { + if (item != null) + { + collection.Add(item); + } + } + + public T2 Cast(int index) where T2 : T + { + return (T2)(object)collection.ElementAt(index); + } + + public static void StaticExtension() + { + } + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs index 3f302a63e..9eb82c286 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs @@ -1496,7 +1496,7 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem Assert.That(method.IsExtensionMethod); Assert.That(method.ReducedFrom, Is.Null); - Assert.That(type.HasExtensionMethods); + Assert.That(type.HasExtensions); } [Test] @@ -1993,5 +1993,23 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem Assert.That(@class.HasAttribute(KnownAttribute.SpecialName)); Assert.That(@struct.HasAttribute(KnownAttribute.SpecialName)); } + + [Test] + public void ExtensionEverything() + { + var extensionEverything = GetTypeDefinition(typeof(ExtensionEverything)); + Assert.That(extensionEverything.IsStatic, Is.True, "ExtensionEverything should be static"); + Assert.That(extensionEverything.HasExtensions, Is.True, "ExtensionEverything should have extensions"); + var info = extensionEverything.ExtensionInfo; + Assert.That(info, Is.Not.Null, "ExtensionEverything should have ExtensionInfo"); + foreach (var method in extensionEverything.Methods) + { + Assert.That(method.IsStatic, Is.True, "Method should be static: " + method.Name); + ExtensionMemberInfo? infoOfImpl = info.InfoOfImplementationMember(method); + Assert.That(infoOfImpl, Is.Not.Null, "Method should have implementation info: " + method.Name); + ExtensionMemberInfo? infoOfExtension = info.InfoOfExtensionMember(infoOfImpl.Value.ExtensionMember); + Assert.That(infoOfExtension, Is.EqualTo(infoOfImpl), "Info of extension member should be equal to info of implementation member: " + method.Name); + } + } } } diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs index a526c793f..1636d5fd8 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs @@ -753,4 +753,24 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem [DispId(11)] void StopRouter(); } + + public static class ExtensionEverything + { + extension(int input) + { + public void Method() { } + public void Method(char c) { } + public string AsString => input.ToString(); + public string Test { + get => "Test"; + set { } + } + public static void StaticMethod() { } + public static void StaticMethod(double x) { } + public static string StaticProperty => "StaticProperty"; + public static void GenericMethod(T value) + { + } + } + } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 35f2fe90c..903c59586 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -318,7 +318,9 @@ namespace ICSharpCode.Decompiler.CSharp return true; if (settings.FixedBuffers && name.StartsWith("<", StringComparison.Ordinal) && name.Contains("__FixedBuffer")) return true; - if (settings.InlineArrays && name.StartsWith("<>y__InlineArray", StringComparison.Ordinal) && name.EndsWith("`1")) + if (settings.InlineArrays && name.StartsWith("<>y__InlineArray", StringComparison.Ordinal) && name.EndsWith("`1", StringComparison.Ordinal)) + return true; + if (settings.ExtensionMembers && name.StartsWith("<>E__", StringComparison.Ordinal)) return true; } else if (type.IsCompilerGenerated(metadata)) @@ -1037,7 +1039,7 @@ namespace ICSharpCode.Decompiler.CSharp break; case HandleKind.MethodDefinition: IMethod method = module.GetDefinition((MethodDefinitionHandle)entity); - syntaxTree.Members.Add(DoDecompile(method, decompileRun, new SimpleTypeResolveContext(method))); + syntaxTree.Members.Add(DoDecompile(method, decompileRun, new SimpleTypeResolveContext(method), null)); if (first) { parentTypeDef = method.DeclaringTypeDefinition; @@ -1054,7 +1056,7 @@ namespace ICSharpCode.Decompiler.CSharp break; case HandleKind.PropertyDefinition: IProperty property = module.GetDefinition((PropertyDefinitionHandle)entity); - syntaxTree.Members.Add(DoDecompile(property, decompileRun, new SimpleTypeResolveContext(property))); + syntaxTree.Members.Add(DoDecompile(property, decompileRun, new SimpleTypeResolveContext(property), null)); if (first) { parentTypeDef = property.DeclaringTypeDefinition; @@ -1361,6 +1363,36 @@ namespace ICSharpCode.Decompiler.CSharp partialTypeInfo = null; } + if (settings.ExtensionMembers) + { + foreach (var group in typeDef.ExtensionInfo?.GetGroups() ?? []) + { + var ext = new ExtensionDeclaration(); + ext.TypeParameters.AddRange(group.Key.DeclaringTypeDefinition.TypeParameters.Select(tp => typeSystemAstBuilder.ConvertTypeParameter(tp))); + ext.ReceiverParameters.Add(typeSystemAstBuilder.ConvertParameter(group.Key.Parameters.Single())); + ext.Constraints.AddRange(group.Key.DeclaringTypeDefinition.TypeParameters.Select(c => typeSystemAstBuilder.ConvertTypeParameterConstraint(c))); + + foreach (var member in group) + { + IMember extMember = member.ExtensionMember; + if (member.ExtensionMember.IsAccessor) + { + extMember = member.ExtensionMember.AccessorOwner; + } + if (entityMap.Contains(extMember) || extMember.MetadataToken.IsNil) + { + // Member is already decompiled. + continue; + } + EntityDeclaration extMemberDecl = DoDecompileExtensionMember(extMember, typeDef.ExtensionInfo, decompileRun, decompilationContext); + ext.Members.Add(extMemberDecl); + entityMap.Add(extMember, extMemberDecl); + } + + typeDecl.Members.Add(ext); + } + } + // Decompile members that are not compiler-generated. foreach (var entity in allOrderedEntities) { @@ -1368,7 +1400,7 @@ namespace ICSharpCode.Decompiler.CSharp { continue; } - DoDecompileMember(entity, recordDecompiler, partialTypeInfo); + DoDecompileMember(entity, recordDecompiler, partialTypeInfo, typeDef.ExtensionInfo); } // Decompile compiler-generated members that are still needed. @@ -1380,7 +1412,7 @@ namespace ICSharpCode.Decompiler.CSharp // Member is already decompiled. continue; } - DoDecompileMember(entity, recordDecompiler, partialTypeInfo); + DoDecompileMember(entity, recordDecompiler, partialTypeInfo, typeDef.ExtensionInfo); } // Add all decompiled members to syntax tree in the correct order. @@ -1470,13 +1502,18 @@ namespace ICSharpCode.Decompiler.CSharp Instrumentation.DecompilerEventSource.Log.DoDecompileTypeDefinition(typeDef.FullName, watch.ElapsedMilliseconds); } - void DoDecompileMember(IEntity entity, RecordDecompiler recordDecompiler, PartialTypeInfo partialType) + void DoDecompileMember(IEntity entity, RecordDecompiler recordDecompiler, PartialTypeInfo partialType, ExtensionInfo extensionInfo) { if (partialType != null && partialType.IsDeclaredMember(entity.MetadataToken)) { return; } + if (settings.ExtensionMembers && extensionInfo != null && entity is IMethod m && extensionInfo.InfoOfImplementationMember(m).HasValue) + { + return; + } + EntityDeclaration entityDecl; switch (entity) { @@ -1497,7 +1534,7 @@ namespace ICSharpCode.Decompiler.CSharp { return; } - entityDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property)); + entityDecl = DoDecompile(property, decompileRun, decompilationContext.WithCurrentMember(property), null); entityMap.Add(property, entityDecl); break; case IMethod method: @@ -1505,7 +1542,7 @@ namespace ICSharpCode.Decompiler.CSharp { return; } - entityDecl = DoDecompile(method, decompileRun, decompilationContext.WithCurrentMember(method)); + entityDecl = DoDecompile(method, decompileRun, decompilationContext.WithCurrentMember(method), null); entityMap.Add(method, entityDecl); foreach (var helper in AddInterfaceImplHelpers(entityDecl, method, typeSystemAstBuilder)) { @@ -1543,6 +1580,19 @@ namespace ICSharpCode.Decompiler.CSharp } } + private EntityDeclaration DoDecompileExtensionMember(IMember extMember, ExtensionInfo info, DecompileRun decompileRun, ITypeResolveContext decompilationContext) + { + switch (extMember) + { + case IProperty p: + return DoDecompile(p, decompileRun, decompilationContext.WithCurrentMember(p), info); + case IMethod m: + return DoDecompile(m, decompileRun, decompilationContext.WithCurrentMember(m), info); + } + + throw new NotSupportedException($"Extension member {extMember} is not supported for decompilation."); + } + EnumValueDisplayMode DetectBestEnumValueDisplayMode(ITypeDefinition typeDef, MetadataFile module) { if (typeDef.HasAttribute(KnownAttribute.Flags)) @@ -1604,7 +1654,7 @@ namespace ICSharpCode.Decompiler.CSharp return firstValue == 0 ? EnumValueDisplayMode.None : EnumValueDisplayMode.FirstOnly; } - EntityDeclaration DoDecompile(IMethod method, DecompileRun decompileRun, ITypeResolveContext decompilationContext) + EntityDeclaration DoDecompile(IMethod method, DecompileRun decompileRun, ITypeResolveContext decompilationContext, ExtensionInfo extensionInfo) { Debug.Assert(decompilationContext.CurrentMember == method); var watch = System.Diagnostics.Stopwatch.StartNew(); @@ -1630,7 +1680,7 @@ namespace ICSharpCode.Decompiler.CSharp } if (methodDefinition.HasBody()) { - DecompileBody(method, methodDecl, decompileRun, decompilationContext); + DecompileBody(method, methodDecl, decompileRun, decompilationContext, extensionInfo); } else if (!method.IsAbstract && method.DeclaringType.Kind != TypeKind.Interface) { @@ -1699,7 +1749,7 @@ namespace ICSharpCode.Decompiler.CSharp return method.ReturnType.Kind == TypeKind.Void && method.Name == "InitializeComponent" && method.DeclaringTypeDefinition.GetNonInterfaceBaseTypes().Any(t => t.FullName == "System.Windows.Forms.Control"); } - void DecompileBody(IMethod method, EntityDeclaration entityDecl, DecompileRun decompileRun, ITypeResolveContext decompilationContext) + void DecompileBody(IMethod method, EntityDeclaration entityDecl, DecompileRun decompileRun, ITypeResolveContext decompilationContext, ExtensionInfo extensionInfo) { try { @@ -1708,6 +1758,14 @@ namespace ICSharpCode.Decompiler.CSharp UseRefLocalsForAccurateOrderOfEvaluation = settings.UseRefLocalsForAccurateOrderOfEvaluation, DebugInfo = DebugInfoProvider }; + int parameterOffset = 0; + if (extensionInfo != null) + { + if (!method.IsStatic) + parameterOffset = 1; // implementation method has an additional receiver parameter + method = extensionInfo.InfoOfExtensionMember(method).Value.ImplementationMethod; + } + var methodDef = metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken); var body = BlockStatement.Null; MethodBodyBlock methodBody; @@ -1727,7 +1785,7 @@ namespace ICSharpCode.Decompiler.CSharp var function = ilReader.ReadIL((MethodDefinitionHandle)method.MetadataToken, methodBody, cancellationToken: CancellationToken); function.CheckInvariant(ILPhase.Normal); - AddAnnotationsToDeclaration(method, entityDecl, function); + AddAnnotationsToDeclaration(method, entityDecl, function, parameterOffset); var localSettings = settings.Clone(); if (IsWindowsFormsInitializeComponentMethod(method)) @@ -1786,9 +1844,9 @@ namespace ICSharpCode.Decompiler.CSharp } } - internal static void AddAnnotationsToDeclaration(IMethod method, EntityDeclaration entityDecl, ILFunction function) + internal static void AddAnnotationsToDeclaration(IMethod method, EntityDeclaration entityDecl, ILFunction function, int parameterOffset = 0) { - int i = 0; + int i = parameterOffset; var parameters = function.Variables.Where(v => v.Kind == VariableKind.Parameter).ToDictionary(v => v.Index); foreach (var parameter in entityDecl.GetChildrenByRole(Roles.Parameter)) { @@ -2023,7 +2081,7 @@ namespace ICSharpCode.Decompiler.CSharp return false; } - EntityDeclaration DoDecompile(IProperty property, DecompileRun decompileRun, ITypeResolveContext decompilationContext) + EntityDeclaration DoDecompile(IProperty property, DecompileRun decompileRun, ITypeResolveContext decompilationContext, ExtensionInfo extensionInfo) { Debug.Assert(decompilationContext.CurrentMember == property); var watch = System.Diagnostics.Stopwatch.StartNew(); @@ -2053,11 +2111,11 @@ namespace ICSharpCode.Decompiler.CSharp bool setterHasBody = property.CanSet && property.Setter.HasBody; if (getterHasBody) { - DecompileBody(property.Getter, getter, decompileRun, decompilationContext); + DecompileBody(property.Getter, getter, decompileRun, decompilationContext, extensionInfo); } if (setterHasBody) { - DecompileBody(property.Setter, setter, decompileRun, decompilationContext); + DecompileBody(property.Setter, setter, decompileRun, decompilationContext, extensionInfo); } if (!getterHasBody && !setterHasBody && !property.IsAbstract && property.DeclaringType.Kind != TypeKind.Interface) { @@ -2113,11 +2171,11 @@ namespace ICSharpCode.Decompiler.CSharp } if (adderHasBody) { - DecompileBody(ev.AddAccessor, ((CustomEventDeclaration)eventDecl).AddAccessor, decompileRun, decompilationContext); + DecompileBody(ev.AddAccessor, ((CustomEventDeclaration)eventDecl).AddAccessor, decompileRun, decompilationContext, null); } if (removerHasBody) { - DecompileBody(ev.RemoveAccessor, ((CustomEventDeclaration)eventDecl).RemoveAccessor, decompileRun, decompilationContext); + DecompileBody(ev.RemoveAccessor, ((CustomEventDeclaration)eventDecl).RemoveAccessor, decompileRun, decompilationContext, null); } if (!adderHasBody && !removerHasBody && !ev.IsAbstract && ev.DeclaringType.Kind != TypeKind.Interface) { diff --git a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs index 65e7fdceb..e91c148c6 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs @@ -36,7 +36,8 @@ namespace ICSharpCode.Decompiler.CSharp CSharp11_0 = 1100, CSharp12_0 = 1200, CSharp13_0 = 1300, - Preview = 1300, + CSharp14_0 = 1400, + Preview = 1400, Latest = 0x7FFFFFFF } } diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index a4f448d2d..f91eba533 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -2417,6 +2417,36 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor EndNode(enumMemberDeclaration); } + public virtual void VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration) + { + StartNode(extensionDeclaration); + WriteAttributes(extensionDeclaration.Attributes); + WriteModifiers(extensionDeclaration.ModifierTokens); + WriteKeyword(ExtensionDeclaration.ExtensionKeywordRole); + WriteTypeParameters(extensionDeclaration.TypeParameters); + Space(policy.SpaceBeforeMethodDeclarationParentheses); + WriteCommaSeparatedListInParenthesis(extensionDeclaration.ReceiverParameters, policy.SpaceWithinMethodDeclarationParentheses); + foreach (Constraint constraint in extensionDeclaration.Constraints) + { + constraint.AcceptVisitor(this); + } + OpenBrace(policy.ClassBraceStyle); + bool first = true; + foreach (var member in extensionDeclaration.Members) + { + if (!first) + { + for (int i = 0; i < policy.MinimumBlankLinesBetweenMembers; i++) + NewLine(); + } + first = false; + member.AcceptVisitor(this); + } + CloseBrace(policy.ClassBraceStyle); + NewLine(); + EndNode(extensionDeclaration); + } + public virtual void VisitEventDeclaration(EventDeclaration eventDeclaration) { StartNode(eventDeclaration); diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs index e761ed231..1f69df969 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpResolver.cs @@ -2215,7 +2215,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver // TODO: maybe make this a property on INamespace? return from c in ns.Types - where c.IsStatic && c.HasExtensionMethods && c.TypeParameters.Count == 0 && lookup.IsAccessible(c, false) + where c.IsStatic && c.HasExtensions && c.TypeParameters.Count == 0 && lookup.IsAccessible(c, false) from m in c.Methods where m.IsExtensionMethod select m; diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs index 2016b429c..5819b7277 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/DepthFirstAstVisitor.cs @@ -157,6 +157,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax VisitChildren(enumMemberDeclaration); } + public virtual void VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration) + { + VisitChildren(extensionDeclaration); + } + public virtual void VisitUsingDeclaration(UsingDeclaration usingDeclaration) { VisitChildren(usingDeclaration); @@ -840,6 +845,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren(enumMemberDeclaration); } + public virtual T VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration) + { + return VisitChildren(extensionDeclaration); + } + public virtual T VisitUsingDeclaration(UsingDeclaration usingDeclaration) { return VisitChildren(usingDeclaration); @@ -1523,6 +1533,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax return VisitChildren(enumMemberDeclaration, data); } + public virtual S VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration, T data) + { + return VisitChildren(extensionDeclaration, data); + } + public virtual S VisitUsingDeclaration(UsingDeclaration usingDeclaration, T data) { return VisitChildren(usingDeclaration, data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs index 37dfd40a3..91bc63694 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/IAstVisitor.cs @@ -16,7 +16,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - namespace ICSharpCode.Decompiler.CSharp.Syntax { /// @@ -136,6 +135,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax void VisitVariableInitializer(VariableInitializer variableInitializer); void VisitFixedFieldDeclaration(FixedFieldDeclaration fixedFieldDeclaration); void VisitFixedVariableInitializer(FixedVariableInitializer fixedVariableInitializer); + void VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration); void VisitSyntaxTree(SyntaxTree syntaxTree); void VisitSimpleType(SimpleType simpleType); @@ -285,6 +285,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitVariableInitializer(VariableInitializer variableInitializer); S VisitFixedFieldDeclaration(FixedFieldDeclaration fixedFieldDeclaration); S VisitFixedVariableInitializer(FixedVariableInitializer fixedVariableInitializer); + S VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration); S VisitSyntaxTree(SyntaxTree syntaxTree); S VisitSimpleType(SimpleType simpleType); @@ -434,6 +435,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax S VisitVariableInitializer(VariableInitializer variableInitializer, T data); S VisitFixedFieldDeclaration(FixedFieldDeclaration fixedFieldDeclaration, T data); S VisitFixedVariableInitializer(FixedVariableInitializer fixedVariableInitializer, T data); + S VisitExtensionDeclaration(ExtensionDeclaration extensionDeclaration, T data); S VisitSyntaxTree(SyntaxTree syntaxTree, T data); S VisitSimpleType(SimpleType simpleType, T data); diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs new file mode 100644 index 000000000..ad5c49839 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ExtensionDeclaration.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2025 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.CSharp.Syntax +{ + public class ExtensionDeclaration : EntityDeclaration + { + public readonly static TokenRole ExtensionKeywordRole = new TokenRole("extension"); + + public override SymbolKind SymbolKind => throw new System.NotImplementedException(); + + public AstNodeCollection TypeParameters { + get { return GetChildrenByRole(Roles.TypeParameter); } + } + + public AstNodeCollection ReceiverParameters { + get { return GetChildrenByRole(Roles.Parameter); } + } + + public AstNodeCollection Constraints { + get { return GetChildrenByRole(Roles.Constraint); } + } + + public AstNodeCollection Members { + get { return GetChildrenByRole(Roles.TypeMemberRole); } + } + + public ExtensionDeclaration() + { + } + + public override void AcceptVisitor(IAstVisitor visitor) + { + visitor.VisitExtensionDeclaration(this); + } + + public override T AcceptVisitor(IAstVisitor visitor) + { + return visitor.VisitExtensionDeclaration(this); + } + + public override S AcceptVisitor(IAstVisitor visitor, T data) + { + return visitor.VisitExtensionDeclaration(this, data); + } + + protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) + { + var o = other as ExtensionDeclaration; + return o != null; + } + } +} diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 6a0078a8d..0abb9a2fb 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -171,10 +171,16 @@ namespace ICSharpCode.Decompiler { paramsCollections = false; } + if (languageVersion < CSharp.LanguageVersion.CSharp14_0) + { + extensionMembers = false; + } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { + if (extensionMembers) + return CSharp.LanguageVersion.CSharp14_0; if (paramsCollections) return CSharp.LanguageVersion.CSharp13_0; if (refReadOnlyParameters || usePrimaryConstructorSyntaxForNonRecordTypes || inlineArrays) @@ -2117,6 +2123,24 @@ namespace ICSharpCode.Decompiler } } + bool extensionMembers = true; + + /// + /// Gets/Sets whether C# 14.0 extension members should be transformed. + /// + [Category("C# 14.0 / VS 202x.yy")] + [Description("DecompilerSettings.ExtensionMembers")] + public bool ExtensionMembers { + get { return extensionMembers; } + set { + if (extensionMembers != value) + { + extensionMembers = value; + OnPropertyChanged(); + } + } + } + bool separateLocalVariableDeclarations = false; /// diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 6d12fb24f..15d677535 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -104,6 +104,7 @@ + @@ -155,6 +156,7 @@ + diff --git a/ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs b/ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs new file mode 100644 index 000000000..f4a080902 --- /dev/null +++ b/ICSharpCode.Decompiler/TypeSystem/ExtensionInfo.cs @@ -0,0 +1,176 @@ +// Copyright (c) 2025 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; + +namespace ICSharpCode.Decompiler.TypeSystem +{ + public class ExtensionInfo + { + readonly Dictionary extensionMemberMap; + readonly Dictionary implementationMemberMap; + + public ExtensionInfo(MetadataModule module, ITypeDefinition extensionContainer) + { + this.extensionMemberMap = new(); + this.implementationMemberMap = new(); + + var metadata = module.MetadataFile.Metadata; + + foreach (var extGroup in extensionContainer.NestedTypes) + { + if (!(extGroup is { Kind: TypeKind.Class, IsSealed: true } + && extGroup.Name.StartsWith("<>E__", System.StringComparison.Ordinal))) + { + continue; + } + + TypeDefinition td = metadata.GetTypeDefinition((TypeDefinitionHandle)extGroup.MetadataToken); + IMethod? marker = null; + bool hasMultipleMarkers = false; + List extensionMethods = []; + + // For easier access to accessors we use SRM + foreach (var h in td.GetMethods()) + { + var method = module.GetDefinition(h); + + if (method.SymbolKind is SymbolKind.Constructor) + continue; + if (method is { Name: "$", IsStatic: true, Parameters.Count: 1 }) + { + if (marker == null) + marker = method; + else + hasMultipleMarkers = true; + continue; + } + + extensionMethods.Add(method); + } + + if (marker == null || hasMultipleMarkers) + continue; + + foreach (var extension in extensionMethods) + { + int expectedTypeParameterCount = extension.TypeParameters.Count + extGroup.TypeParameterCount; + bool hasInstance = !extension.IsStatic; + int parameterOffset = hasInstance ? 1 : 0; + int expectedParameterCount = extension.Parameters.Count + parameterOffset; + TypeParameterSubstitution subst = new TypeParameterSubstitution([], [.. extGroup.TypeArguments, .. extension.TypeArguments]); + + bool IsMatchingImplementation(IMethod impl) + { + if (!impl.IsStatic) + return false; + if (extension.Name != impl.Name) + return false; + if (expectedTypeParameterCount != impl.TypeParameters.Count) + return false; + if (expectedParameterCount != impl.Parameters.Count) + return false; + if (hasInstance) + { + IType ti = impl.Parameters[0].Type.AcceptVisitor(subst); + IType tm = marker.Parameters.Single().Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(ti, tm)) + return false; + } + for (int i = 0; i < extension.Parameters.Count; i++) + { + IType ti = impl.Parameters[i + parameterOffset].Type.AcceptVisitor(subst); + IType tm = extension.Parameters[i].Type; + if (!NormalizeTypeVisitor.TypeErasure.EquivalentTypes(ti, tm)) + return false; + } + return NormalizeTypeVisitor.TypeErasure.EquivalentTypes( + impl.ReturnType.AcceptVisitor(subst), + extension.ReturnType + ); + } + + foreach (var impl in extensionContainer.Methods) + { + if (!IsMatchingImplementation(impl)) + continue; + var emi = new ExtensionMemberInfo(marker, extension, impl); + extensionMemberMap[extension] = emi; + implementationMemberMap[impl] = emi; + } + } + } + + } + + public ExtensionMemberInfo? InfoOfExtensionMember(IMethod method) + { + return this.extensionMemberMap.TryGetValue(method, out var value) ? value : null; + } + + public ExtensionMemberInfo? InfoOfImplementationMember(IMethod method) + { + return this.implementationMemberMap.TryGetValue(method, out var value) ? value : null; + } + + public IEnumerable> GetGroups() + { + return this.extensionMemberMap.Values.GroupBy(x => x.ExtensionMarkerMethod); + } + } + + public readonly struct ExtensionMemberInfo(IMethod marker, IMethod extension, IMethod implementation) + { + /// + /// Metadata-only method called '<Extension>$'. Has the C# signature for the extension declaration. + /// + /// extension(ReceiverType name) {} -> void <Extension>$(ReceiverType name) {} + /// + public readonly IMethod ExtensionMarkerMethod = marker; + /// + /// Metadata-only method with a signature as declared in C# within the extension declaration. + /// This could also be an accessor of an extension property. + /// + public readonly IMethod ExtensionMember = extension; + /// + /// The actual implementation method in the outer class. The signature is a concatenation + /// of the extension marker and the extension member's signatures. + /// + public readonly IMethod ImplementationMethod = implementation; + + /// + /// This is the enclosing static class. + /// + public ITypeDefinition ExtensionContainer => ImplementationMethod.DeclaringTypeDefinition!; + + /// + /// This is the compiler-generated class containing the extension members. Has type parameters + /// from the extension declaration with minimal constraints. + /// + public ITypeDefinition ExtensionGroupingType => ExtensionMember.DeclaringTypeDefinition!; + + /// + /// This class holds the type parameters for the extension declaration with full fidelity of C# constraints. + /// + public ITypeDefinition ExtensionMarkerType => ExtensionMarkerMethod.DeclaringTypeDefinition!; + } +} diff --git a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs index f7d07a2c6..9101162d1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ITypeDefinition.cs @@ -28,6 +28,7 @@ namespace ICSharpCode.Decompiler.TypeSystem /// public interface ITypeDefinition : ITypeDefinitionOrUnknown, IType, IEntity { + ExtensionInfo? ExtensionInfo { get; } IReadOnlyList NestedTypes { get; } IReadOnlyList Members { get; } @@ -71,10 +72,10 @@ namespace ICSharpCode.Decompiler.TypeSystem new IType? DeclaringType { get; } // solves ambiguity between IType.DeclaringType and IEntity.DeclaringType /// - /// Gets whether this type contains extension methods. + /// Gets whether this type contains extension methods or C# 14 extensions. /// - /// This property is used to speed up the search for extension methods. - bool HasExtensionMethods { get; } + /// This property is used to speed up the search for extension members. + bool HasExtensions { get; } /// /// The nullability specified in the [NullableContext] attribute on the type. diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs index 632a3e4cc..a312bd8a3 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeDefinition.cs @@ -41,6 +41,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation // eagerly loaded: readonly FullTypeName fullTypeName; readonly TypeAttributes attributes; + public TypeKind Kind { get; } public bool IsByRefLike { get; } public bool IsReadOnly { get; } @@ -48,7 +49,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation public IReadOnlyList TypeParameters { get; } public KnownTypeCode KnownTypeCode { get; } public IType EnumUnderlyingType { get; } - public bool HasExtensionMethods { get; } + public bool HasExtensions { get; } public Nullability NullableContext { get; } // lazy-loaded: @@ -132,7 +133,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation else { this.Kind = TypeKind.Class; - this.HasExtensionMethods = this.IsStatic + this.HasExtensions = this.IsStatic && (module.TypeSystemOptions & TypeSystemOptions.ExtensionMethods) == TypeSystemOptions.ExtensionMethods && td.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.Extension); } @@ -143,6 +144,22 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return $"{MetadataTokens.GetToken(handle):X8} {fullTypeName}"; } + private ExtensionInfo extensionInfo; + + public ExtensionInfo ExtensionInfo { + get { + if (!HasExtensions) + return null; + var extensionInfo = LazyInit.VolatileRead(ref this.extensionInfo); + if (extensionInfo != null) + return extensionInfo; + extensionInfo = new ExtensionInfo(module, this); + if ((module.TypeSystemOptions & TypeSystemOptions.Uncached) != 0) + return extensionInfo; + return LazyInit.GetOrSet(ref this.extensionInfo, extensionInfo); + } + } + ITypeDefinition[] nestedTypes; public IReadOnlyList NestedTypes { diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs index 4f1b9cc8a..82f6f79f4 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MinimalCorlib.cs @@ -173,7 +173,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation IType IType.DeclaringType => null; IType IEntity.DeclaringType => null; - bool ITypeDefinition.HasExtensionMethods => false; + bool ITypeDefinition.HasExtensions => false; + ExtensionInfo ITypeDefinition.ExtensionInfo => null; bool ITypeDefinition.IsReadOnly => false; TypeKind IType.Kind => typeKind; diff --git a/ICSharpCode.ILSpyX/Analyzers/Builtin/TypeExtensionMethodsAnalyzer.cs b/ICSharpCode.ILSpyX/Analyzers/Builtin/TypeExtensionMethodsAnalyzer.cs index fbf8470a5..0823b450a 100644 --- a/ICSharpCode.ILSpyX/Analyzers/Builtin/TypeExtensionMethodsAnalyzer.cs +++ b/ICSharpCode.ILSpyX/Analyzers/Builtin/TypeExtensionMethodsAnalyzer.cs @@ -46,7 +46,7 @@ namespace ICSharpCode.ILSpyX.Analyzers.Builtin IEnumerable ScanType(ITypeDefinition analyzedType, ITypeDefinition type, AnalyzerContext context) { - if (!type.HasExtensionMethods) + if (!type.HasExtensions) yield break; if (analyzedType.ParentModule?.MetadataFile == null) diff --git a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs index fe229607a..2361c8b9a 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs @@ -242,6 +242,7 @@ namespace ICSharpCode.ILSpy case "class": case "interface": case "delegate": + case "extension": color = referenceTypeKeywordsColor; break; case "record": diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 6d66c4736..5d56f5a09 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -117,6 +117,7 @@ namespace ICSharpCode.ILSpy new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp11_0.ToString(), "C# 11.0 / VS 2022.4"), new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp12_0.ToString(), "C# 12.0 / VS 2022.8"), new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp13_0.ToString(), "C# 13.0 / VS 2022.12"), + new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp14_0.ToString(), "C# 14.0 / VS 202x.yy"), }; } return versions;