diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
index 499e82c4c..2ffb392ac 100644
--- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
+++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
@@ -575,7 +575,16 @@ namespace System.Runtime.CompilerServices
{
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 d38245d46..f6cb1035d 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 f68632b75..ad7823a00 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -663,6 +663,18 @@ namespace ICSharpCode.Decompiler.Tests
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 000000000..e17d9fd89
--- /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 000000000..d1e729f65
--- /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 000000000..46e8318a3
--- /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 631830e39..b18a71d38 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3684.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3684.cs
@@ -15,7 +15,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
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 0fd1294a1..3c21fdbf7 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -1703,6 +1703,12 @@ namespace ICSharpCode.Decompiler.CSharp
{
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 @@ namespace ICSharpCode.Decompiler.CSharp
{
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 4447be86d..8aeb2d409 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs
@@ -97,7 +97,10 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
}
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 0f7902524..aa21c67a4 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
@@ -190,7 +190,10 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
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 84a80cc18..d62c6b875 100644
--- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
@@ -200,7 +200,7 @@ namespace ICSharpCode.Decompiler.CSharp
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 @@ namespace ICSharpCode.Decompiler.CSharp
// 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 @@ namespace ICSharpCode.Decompiler.CSharp
{
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 d286f21cf..b9869dc68 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs
@@ -59,6 +59,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
Async = 0x10000,
Ref = 0x20000,
Required = 0x40000,
+ Closed = 0x80000,
VisibilityMask = Private | Internal | Protected | Public,
@@ -76,7 +77,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
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 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
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 67b2dab91..61ee14271 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs
@@ -186,6 +186,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
"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 955003893..3717e75e5 100644
--- a/ICSharpCode.Decompiler/DecompilerSettings.cs
+++ b/ICSharpCode.Decompiler/DecompilerSettings.cs
@@ -177,10 +177,16 @@ namespace ICSharpCode.Decompiler
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 @@ namespace ICSharpCode.Decompiler
}
}
+ 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 52a20c337..227ae1afe 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
@@ -120,11 +120,14 @@ namespace ICSharpCode.Decompiler.TypeSystem
// 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 @@ namespace ICSharpCode.Decompiler.TypeSystem
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 7e3470336..609dfd1dd 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