diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index 499e82c4c3..2ffb392acd 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -575,7 +575,16 @@ public static async Task CompileCSharp(string sourceFileName, C { string depSourcePath = Path.GetFullPath(Path.Combine( Path.GetDirectoryName(sourceFileName), match.Groups[1].Value)); - var depResults = await CompileCSharp(depSourcePath, flags | CompilerOptions.Library).ConfigureAwait(false); + // Compile the dependency next to the main output, so that the assembly resolver + // finds it when the main assembly is decompiled. + string depOutputFileName = null; + if (outputFileName != null) + { + depOutputFileName = Path.Combine( + Path.GetDirectoryName(Path.GetFullPath(outputFileName)), + Path.GetFileNameWithoutExtension(depSourcePath) + GetSuffix(flags | CompilerOptions.Library) + ".dll"); + } + var depResults = await CompileCSharp(depSourcePath, flags | CompilerOptions.Library, depOutputFileName).ConfigureAwait(false); dependencyAssemblies.Add(Path.GetFullPath(depResults.PathToAssembly)); } diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index d38245d468..f6cb1035db 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -221,6 +221,12 @@ + + + + + + diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index f68632b758..ad7823a001 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -663,6 +663,18 @@ public async Task ExtensionEverything([ValueSource(nameof(roslyn5OrNewerOptions) await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview | CompilerOptions.NullableEnable); } + [Test] + public async Task ClosedHierarchies([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions | CompilerOptions.Preview); + } + + [Test] + public async Task ClosedHierarchiesCrossAssembly([ValueSource(nameof(roslyn5OrNewerOptions))] 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/ClosedHierarchies.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchies.cs new file mode 100644 index 0000000000..e17d9fd899 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchies.cs @@ -0,0 +1,74 @@ +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class ClosedHierarchies + { + public closed class Animal + { + public string Name { get; } + + protected Animal() + { + Name = "unnamed"; + } + + protected Animal(string name) + { + Name = name; + } + } + + public class Cat : Animal + { + } + + public sealed class Dog : Animal + { + public Dog() + : base("Dog") + { + } + } + + public closed class Job + { + public required string Title { get; set; } + } + + public sealed class CompileJob : Job + { + } + + public closed class Tree + { + } + + public sealed class Leaf : Tree + { + } + + public sealed class ArrayLeaf : Tree + { + } + } + public closed record JobStatus; + internal record JobStatusCanceled : JobStatus; + public record JobStatusQueued : JobStatus; + public record JobStatusRunning(int PercentComplete) : JobStatus; + internal closed class State + { + } + internal sealed class StateActive : State + { + } +} +#if !EXPECTED_OUTPUT +// The .NET 11 preview 5 BCL does not ship ClosedAttribute yet; the compiler requires +// every assembly using the 'closed' modifier to declare it (matched by full name). +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal sealed class ClosedAttribute : Attribute + { + } +} +#endif diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchiesCrossAssembly.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchiesCrossAssembly.cs new file mode 100644 index 0000000000..d1e729f65e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchiesCrossAssembly.cs @@ -0,0 +1,9 @@ +// #dependency ClosedHierarchiesCrossAssembly.dep.cs +using CrossAssemblyClosed; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + public closed record LocalMessage; + public record LocalTextMessage(string Text) : LocalMessage; + public record Sedan(int Doors) : Car(Doors); +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchiesCrossAssembly.dep.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchiesCrossAssembly.dep.cs new file mode 100644 index 0000000000..46e8318a39 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ClosedHierarchiesCrossAssembly.dep.cs @@ -0,0 +1,17 @@ +namespace CrossAssemblyClosed +{ + public closed record Vehicle; + public record Car(int Doors) : Vehicle; + public sealed record Truck(double PayloadTons) : Vehicle; +} + +// Public so that the main test assembly can use the 'closed' modifier without +// declaring its own copy of the attribute (the shape the BCL will provide once +// ClosedAttribute ships). +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class ClosedAttribute : Attribute + { + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3684.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3684.cs index 631830e39d..b18a71d38f 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3684.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3684.cs @@ -15,7 +15,7 @@ public class DerivedClass : BaseClass, IInterface { T IInterface.Convert(T input) { - return ((BaseClass)this).Convert(input); + return Convert(input); } } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 0fd1294a1d..3c21fdbf71 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -1703,6 +1703,12 @@ EntityDeclaration DoDecompile(ITypeDefinition typeDef, DecompileRun decompileRun { RemoveAttribute(typeDecl, KnownAttribute.Required); } + if (settings.ClosedHierarchies && RemoveAttribute(typeDecl, KnownAttribute.Closed)) + { + // closed classes are implicitly abstract + typeDecl.Modifiers |= Modifiers.Closed; + typeDecl.Modifiers &= ~Modifiers.Abstract; + } if (typeDecl.ClassType == ClassType.Enum) { Debug.Assert(typeDef.Kind == TypeKind.Enum); @@ -2007,6 +2013,10 @@ EntityDeclaration DoDecompile(IMethod method, DecompileRun decompileRun, ITypeRe { RemoveObsoleteAttribute(methodDecl, "Constructors of types with required members are not supported in this version of your compiler."); } + if (method.IsConstructor && settings.ClosedHierarchies) + { + RemoveCompilerFeatureRequiredAttribute(methodDecl, "ClosedClasses"); + } return methodDecl; bool IsTypeHierarchyKnown(IType type) diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs index 4447be86dd..8aeb2d409f 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs @@ -97,7 +97,10 @@ public void Write( } w.WriteElementString("OutputType", outputType); - w.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); + // C# 15 is still in preview; the compiler only accepts -langversion:preview for it. + w.WriteElementString("LangVersion", project.LanguageVersion >= LanguageVersion.CSharp15_0 + ? "preview" + : project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); w.WriteElementString("CheckForOverflowUnderflow", project.CheckForOverflowUnderflow ? "true" : "false"); w.WriteElementString("AssemblyName", module.Name); diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index 0f79025246..aa21c67a42 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -190,7 +190,10 @@ static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType) static void WriteProjectInfo(XmlTextWriter xml, IProjectInfoProvider project) { - xml.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); + // C# 15 is still in preview; the compiler only accepts -langversion:preview for it. + xml.WriteElementString("LangVersion", project.LanguageVersion >= LanguageVersion.CSharp15_0 + ? "preview" + : project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); xml.WriteElementString("AllowUnsafeBlocks", TrueString); xml.WriteElementString("CheckForOverflowUnderflow", project.CheckForOverflowUnderflow ? TrueString : FalseString); diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs index 84a80cc185..d62c6b8752 100644 --- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs @@ -200,7 +200,7 @@ bool IsAutoSetter(IMethod method, [NotNullWhen(true)] out IField? field) IMethod? chainedCtor = (IMethod?)FindChainedCtor(body)?.MemberDefinition; ctorChainMap[method] = chainedCtor; - if (chainedCtor != null && chainedCtor.DeclaringTypeDefinition!.Equals(recordTypeDef)) + if (chainedCtor != null && recordTypeDef.Equals(chainedCtor.DeclaringTypeDefinition)) { continue; } @@ -233,7 +233,7 @@ bool IsAutoSetter(IMethod method, [NotNullWhen(true)] out IField? field) // follow this rule. // we don't have to check the full chain, because the C# compiler enforces that // there are no loops in the ctor call graph. - if (target == null || !target.DeclaringTypeDefinition!.Equals(recordTypeDef)) + if (target == null || !recordTypeDef.Equals(target.DeclaringTypeDefinition)) { guessedPrimaryCtor = null; break; @@ -522,6 +522,12 @@ private bool IsAllowedAttribute(IAttribute attribute) { case "System.Runtime.CompilerServices.CompilerGeneratedAttribute": return true; + case "System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute": + // closed records carry CompilerFeatureRequired("ClosedClasses") on all constructors + return settings.ClosedHierarchies + && attribute.FixedArguments.Length == 1 + && attribute.FixedArguments[0].Value is string feature + && feature == "ClosedClasses"; default: return false; } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs index d286f21cf8..b9869dc685 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs @@ -59,6 +59,7 @@ public enum Modifiers Async = 0x10000, Ref = 0x20000, Required = 0x40000, + Closed = 0x80000, VisibilityMask = Private | Internal | Protected | Public, @@ -76,7 +77,7 @@ public static class CSharpModifiers Modifiers.Public, Modifiers.Private, Modifiers.Protected, Modifiers.Internal, Modifiers.New, Modifiers.Unsafe, - Modifiers.Static, Modifiers.Abstract, Modifiers.Virtual, Modifiers.Sealed, Modifiers.Override, + Modifiers.Static, Modifiers.Abstract, Modifiers.Virtual, Modifiers.Sealed, Modifiers.Closed, Modifiers.Override, Modifiers.Required, Modifiers.Readonly, Modifiers.Volatile, Modifiers.Ref, Modifiers.Extern, Modifiers.Partial, Modifiers.Const, @@ -126,6 +127,8 @@ public static string GetModifierName(Modifiers modifier) return "ref"; case Modifiers.Required: return "required"; + case Modifiers.Closed: + return "closed"; case Modifiers.Any: // even though it's used for pattern matching only, 'any' needs to be in this list to be usable in the AST return "any"; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs index 67b2dab919..61ee14271e 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs @@ -186,6 +186,7 @@ public class RemoveEmbeddedAttributes : DepthFirstAstVisitor, IAstTransform "System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute", "System.Runtime.CompilerServices.RequiredMemberAttribute", "System.Runtime.CompilerServices.IsExternalInit", + "System.Runtime.CompilerServices.ClosedAttribute", }; public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration) diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 955003893e..3717e75e54 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -177,10 +177,16 @@ public void SetLanguageVersion(CSharp.LanguageVersion languageVersion) extensionMembers = false; firstClassSpanTypes = false; } + if (languageVersion < CSharp.LanguageVersion.CSharp15_0) + { + closedHierarchies = false; + } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { + if (closedHierarchies) + return CSharp.LanguageVersion.CSharp15_0; if (extensionMembers || firstClassSpanTypes) return CSharp.LanguageVersion.CSharp14_0; if (paramsCollections) @@ -2194,6 +2200,24 @@ public bool ExtensionMembers { } } + bool closedHierarchies = true; + + /// + /// Gets/Sets whether the closed modifier should be reconstructed on closed hierarchies. + /// + [Category("C# 15.0 / VS 2026")] + [Description("DecompilerSettings.ClosedHierarchies")] + public bool ClosedHierarchies { + get { return closedHierarchies; } + set { + if (closedHierarchies != value) + { + closedHierarchies = value; + OnPropertyChanged(); + } + } + } + bool firstClassSpanTypes = true; /// diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs index 52a20c3375..227ae1afe1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs @@ -120,11 +120,14 @@ public enum KnownAttribute // C# 14 attributes: ExtensionMarker, + + // C# 15 attributes: + Closed, } public static class KnownAttributes { - internal const int Count = (int)KnownAttribute.ExtensionMarker + 1; + internal const int Count = (int)KnownAttribute.Closed + 1; static readonly TopLevelTypeName[] typeNames = new TopLevelTypeName[Count]{ default, @@ -200,6 +203,8 @@ public static class KnownAttributes new TopLevelTypeName("System.Runtime.CompilerServices", "InlineArrayAttribute"), // C# 14 attributes: new TopLevelTypeName("System.Runtime.CompilerServices", "ExtensionMarkerAttribute"), + // C# 15 attributes: + new TopLevelTypeName("System.Runtime.CompilerServices", "ClosedAttribute"), }; public static ref readonly TopLevelTypeName GetTypeName(this KnownAttribute attr) diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 7e34703366..609dfd1dda 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -360,6 +360,9 @@ Are you sure you want to continue? User-defined checked operators + + Use the closed modifier on closed hierarchies + Covariant return types