diff --git a/.scripts/update_ilspy.sh b/.scripts/update_ilspy.sh new file mode 100755 index 00000000..8c5dd727 --- /dev/null +++ b/.scripts/update_ilspy.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ -z "${ILSPY_DIR:-}" ]]; then + echo "Error: ILSPY_DIR is not set." + exit 1 +fi + +if [[ ! -d "$ILSPY_DIR" ]]; then + echo "Error: ILSPY_DIR does not exist: $ILSPY_DIR" + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)" +DEPS_DIR="$WORKSPACE_DIR/modules/gdsdecomp/godot-mono-decomp/dependencies" +CSPROJ_PATH="$WORKSPACE_DIR/modules/gdsdecomp/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj" + +mkdir -p "$DEPS_DIR" + +echo "Packing ILSpy packages into: $DEPS_DIR" +( + cd "$ILSPY_DIR" + dotnet pack -c Release ICSharpCode.Decompiler -o "$DEPS_DIR" + dotnet pack -c Release ICSharpCode.ILSpyX -o "$DEPS_DIR" +) + +latest_package_version() { + local package_name="$1" + local newest + + newest="$( + ls -t "$DEPS_DIR/${package_name}."*.nupkg 2>/dev/null \ + | while IFS= read -r path; do + [[ "$path" == *.snupkg ]] && continue + printf '%s\n' "$path" + done \ + | head -n 1 + )" + + if [[ -z "$newest" ]]; then + echo "Error: Could not find packed package for $package_name in $DEPS_DIR" + exit 1 + fi + + local filename version + filename="${newest##*/}" + version="${filename#${package_name}.}" + version="${version%.nupkg}" + printf '%s\n' "$version" +} + +prune_older_packages() { + local package_name="$1" + local keep_version="$2" + + shopt -s nullglob + local files=( + "$DEPS_DIR/${package_name}."*.nupkg + "$DEPS_DIR/${package_name}."*.snupkg + ) + shopt -u nullglob + + for file in "${files[@]}"; do + case "$file" in + "$DEPS_DIR/${package_name}.${keep_version}.nupkg" | "$DEPS_DIR/${package_name}.${keep_version}.snupkg") + ;; + *) + rm -f "$file" + ;; + esac + done +} + +decompiler_version="$(latest_package_version "ICSharpCode.Decompiler")" +ilspyx_version="$(latest_package_version "ICSharpCode.ILSpyX")" + +prune_older_packages "ICSharpCode.Decompiler" "$decompiler_version" +prune_older_packages "ICSharpCode.ILSpyX" "$ilspyx_version" + +if [[ ! -f "$CSPROJ_PATH" ]]; then + echo "Error: csproj not found: $CSPROJ_PATH" + exit 1 +fi + +# Remove existing Decompiler/ILSpyX references (active or commented), then insert updated package refs. +sed -i.bak '/PackageReference Include="ICSharpCode\.Decompiler"/d' "$CSPROJ_PATH" +sed -i.bak '/PackageReference Include="ICSharpCode\.ILSpyX"/d' "$CSPROJ_PATH" +sed -i.bak '/ProjectReference Include=".*ICSharpCode\.Decompiler\.csproj"/d' "$CSPROJ_PATH" +sed -i.bak '/ProjectReference Include=".*ICSharpCode\.ILSpyX\.csproj"/d' "$CSPROJ_PATH" + +sed -i.bak "/\\ + +" "$CSPROJ_PATH" + +rm -f "$CSPROJ_PATH.bak" + +echo "Updated $CSPROJ_PATH" +echo " - ICSharpCode.Decompiler: $decompiler_version" +echo " - ICSharpCode.ILSpyX: $ilspyx_version" diff --git a/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs b/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs new file mode 100644 index 00000000..43351cdf --- /dev/null +++ b/godot-mono-decomp/GodotMonoDecomp/CollectionExpressionOutputVisitor.cs @@ -0,0 +1,65 @@ +using System.IO; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.CSharp.Syntax; + +namespace GodotMonoDecomp; + +internal sealed class CollectionExpressionArrayAnnotation +{ + public static readonly CollectionExpressionArrayAnnotation Instance = new(); + + private CollectionExpressionArrayAnnotation() + { + } +} + +internal sealed class CollectionExpressionSpreadElementAnnotation +{ + public static readonly CollectionExpressionSpreadElementAnnotation Instance = new(); + + private CollectionExpressionSpreadElementAnnotation() + { + } +} + +public class GodotCSharpOutputVisitor : CSharpOutputVisitor +{ + public GodotCSharpOutputVisitor(TextWriter w, CSharpFormattingOptions formattingOptions) + : base(w, formattingOptions) + { + } + + public override void VisitArrayInitializerExpression(ArrayInitializerExpression arrayInitializerExpression) + { + if (arrayInitializerExpression.Annotation() == null) + { + base.VisitArrayInitializerExpression(arrayInitializerExpression); + return; + } + + StartNode(arrayInitializerExpression); + WriteToken(Roles.LBracket); + + bool first = true; + foreach (var element in arrayInitializerExpression.Elements) + { + if (!first) + { + WriteToken(Roles.Comma); + Space(); + } + + if (element.Annotation() != null) + { + WriteToken(BinaryOperatorExpression.RangeRole); + } + + element.AcceptVisitor(this); + first = false; + } + + WriteToken(Roles.RBracket); + EndNode(arrayInitializerExpression); + } +} diff --git a/godot-mono-decomp/GodotMonoDecomp/Common.cs b/godot-mono-decomp/GodotMonoDecomp/Common.cs index 8ff2eff3..995174f2 100644 --- a/godot-mono-decomp/GodotMonoDecomp/Common.cs +++ b/godot-mono-decomp/GodotMonoDecomp/Common.cs @@ -277,4 +277,86 @@ public static Guid GenerateDeterministicGuidFromString(string input) return new Guid(hashBytes); } } + + private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute"; + public static bool RemoveGeneratedCodeAttributes(ICSharpCode.Decompiler.CSharp.Syntax.AstNodeCollection attributeSections, string generatorName) + { + bool removedAny = false; + foreach (var section in attributeSections.ToArray()) + { + foreach (var attribute in section.Attributes.ToArray()) + { + if (IsGeneratedCodeAttributeForTool(attribute, generatorName)) + { + attribute.Remove(); + removedAny = true; + } + } + + if (section.Attributes.Count == 0) + { + section.Remove(); + } + } + + return removedAny; + } + + public static bool HasMatchingShortAttributeName(string typeName, string expectedShortName) + { + if (typeName.StartsWith("global::", StringComparison.Ordinal)) + { + typeName = typeName.Substring("global::".Length); + } + + int lastDot = typeName.LastIndexOf('.'); + if (lastDot >= 0 && lastDot < typeName.Length - 1) + { + typeName = typeName.Substring(lastDot + 1); + } + + if (typeName.EndsWith("Attribute", StringComparison.Ordinal)) + { + typeName = typeName.Substring(0, typeName.Length - "Attribute".Length); + } + + return string.Equals(typeName, expectedShortName, StringComparison.Ordinal); + } + + public static bool IsAttribute(ICSharpCode.Decompiler.CSharp.Syntax.Attribute attribute, string expectedFullName, string expectedShortName) + { + if (attribute.Type.Annotation() is { Type: { } typeResult }) + { + if (string.Equals(typeResult.FullName, expectedFullName, StringComparison.Ordinal)) + { + return true; + } + } + + if (attribute.GetSymbol() is IMethod method && method.DeclaringType != null) + { + if (string.Equals(method.DeclaringType.FullName, expectedFullName, StringComparison.Ordinal)) + { + return true; + } + } + + return HasMatchingShortAttributeName(attribute.Type.ToString(), expectedShortName); + } + + + public static bool IsGeneratedCodeAttributeForTool(ICSharpCode.Decompiler.CSharp.Syntax.Attribute attribute, string generatorName) + { + if (!IsAttribute(attribute, GeneratedCodeAttributeFullName, "GeneratedCode")) + { + return false; + } + + if (attribute.Arguments.FirstOrDefault() is ICSharpCode.Decompiler.CSharp.Syntax.PrimitiveExpression { Value: string toolName }) + { + return string.Equals(toolName, generatorName, StringComparison.Ordinal); + } + + return false; + } } diff --git a/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs b/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs index 8eb716b7..e91248dc 100644 --- a/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs +++ b/godot-mono-decomp/GodotMonoDecomp/DotNetDepInfo.cs @@ -233,7 +233,7 @@ public static string GetDepPath(string assemblyPath) return null; } - public async Task StartResolvePackageAndCheckHash(CancellationToken cancellationToken) + public async Task StartResolvePackageAndCheckHash(bool checkOnline, CancellationToken cancellationToken) { if (!Serviceable || Type != "package" || string.IsNullOrEmpty(Sha512) || !Sha512.StartsWith("sha512-")) { @@ -245,7 +245,7 @@ public async Task StartResolvePackageAndCheckHash(CancellationToken cancellation string? hash; try { - hash = await NugetDetails.ResolvePackageAndGetContentHash(Name, Version, cancellationToken); + hash = await NugetDetails.ResolvePackageAndGetContentHash(Name, Version, checkOnline, cancellationToken); } catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound) { diff --git a/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs b/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs new file mode 100644 index 00000000..796c55b5 --- /dev/null +++ b/godot-mono-decomp/GodotMonoDecomp/FixSwitchExpressionCasts.cs @@ -0,0 +1,88 @@ +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; +using ICSharpCode.Decompiler.TypeSystem; + +namespace GodotMonoDecomp; + +/// +/// Intended to fix switch expressions that do not have enough context to determine the best type as a result of a parent member reference expression. +/// +/// +/// e.g: +/// ```c# +/// public Animal GetAnimal(AnimalType animalType) +/// { +/// return (animalType switch +/// { +/// // at least one of these should have a cast to 'Animal' or the member reference expression below will cause the switch expression to fail to compile. +/// AnimalType.Dog => new Dog(), +/// AnimalType.Fish => new Fish(), +/// _ => throw new ArgumentException("Invalid animal type") +/// }).ToValidated(); +/// } +/// ``` +/// +public class FixSwitchExpressionCasts : DepthFirstAstVisitor, IAstTransform +{ + private TransformContext? context; + + public void Run(AstNode rootNode, TransformContext context) + { + this.context = context; + rootNode.AcceptVisitor(this); + } + + private static bool IsCastableSwitchSectionExpressionBody(Expression body) + { + return body is not PrimitiveExpression && body is not NullReferenceExpression && body is not ThrowExpression; + } + + public override void VisitSwitchExpression(SwitchExpression switchExpr) + { + if (context?.TypeSystemAstBuilder is null || switchExpr.Parent is not MemberReferenceExpression) + { + base.VisitSwitchExpression(switchExpr); + return; + } + var resolved = switchExpr.GetResolveResult(); + + if (!resolved.IsError && switchExpr.SwitchSections.Count > 1 && resolved.Type is not null) + { + if (!switchExpr.SwitchSections.Any(s => s.Body is CastExpression)) + { + var resolvedTypeDefinition = resolved.Type.GetDefinition(); + HashSet allDefs = []; + var mismatchedSections = switchExpr.SwitchSections.Where(s => { + if (!IsCastableSwitchSectionExpressionBody(s.Body)) + { + return false; + } + var rr = s.Body.GetResolveResult(); + if (rr is not null && !rr.IsError) + { + var def = rr.Type.GetDefinition(); + if (def is not null) + { + allDefs.Add(def); + if (!def.Equals(resolvedTypeDefinition)) + { + return true; + } + } + + } + return false; + }).ToArray(); + if (mismatchedSections.Length > 0 && allDefs.Count > 1) { + var first = mismatchedSections.FirstOrDefault()!; + var body = first.Body; + first.Body = null; + first.Body = new CastExpression(context.TypeSystemAstBuilder.ConvertType(resolved.Type), body); + } + } + } + + base.VisitSwitchExpression(switchExpr); + } +} diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs b/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs index 2d6a37b3..cb027605 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotModuleDecompiler.cs @@ -26,7 +26,7 @@ public class GodotModule public readonly Guid moduleGuid; public readonly IDebugInfoProvider? debugInfoProvider; public readonly string? SubDirectory; - public Dictionary fileMap; + public Dictionary> fileMap; private GodotProjectDecompiler ProjectDecompiler; @@ -246,39 +246,50 @@ public GodotModuleDecompiler(string assemblyPath, string[]? originalProjectFiles } - if (Settings.VerifyNuGetPackageIsFromNugetOrg) + foreach (var module in new List { MainModule }.Concat(AdditionalModules)) { - foreach (var module in new List { MainModule }.Concat(AdditionalModules)) + foreach (var dep in module.depInfo?.deps.Where(d => + d is { Type: "package" } && + (!ProjectFileWriterGodotStyle.ImplicitGodotReferences.Contains(d.Name) || + string.Equals(d.Name, "GodotSharp", StringComparison.OrdinalIgnoreCase))) ?? []) { - foreach (var dep in module.depInfo?.deps.Where(d => - d is { Type: "package" } && - (!ProjectFileWriterGodotStyle.ImplicitGodotReferences.Contains(d.Name) || - string.Equals(d.Name, "GodotSharp", StringComparison.OrdinalIgnoreCase))) ?? []) - { - packageHashTasks.Add( - Task.Run(async () => await dep.StartResolvePackageAndCheckHash(packageHashTaskCancelSrc.Token), packageHashTaskCancelSrc.Token) - ); - } + packageHashTasks.Add( + Task.Run(async () => await dep.StartResolvePackageAndCheckHash(Settings.VerifyNuGetPackageIsFromNugetOrg, packageHashTaskCancelSrc.Token), packageHashTaskCancelSrc.Token) + ); } } HashSet excludeSubdirs = AdditionalModules.Select(module => module.SubDirectory ?? "").Where(subdir => !string.IsNullOrEmpty(subdir)).ToHashSet(); + HashSet GetGodotClassHandles(GodotModule module, IEnumerable handles) + { + var handleSet = handles.ToHashSet(); + var decompiler = module.CreateCSharpDecompilerWithPartials(handleSet); + var godotHandles = new HashSet(); + foreach (var h in handleSet) + { + var typeDef = decompiler.TypeSystem.MainModule.GetDefinition(h); + if (typeDef != null && GodotStuff.IsGodotClass(typeDef)) + { + godotHandles.Add(h); + } + } + return godotHandles; + } + var typesToDecompile = MainModule.GetProjectDecompiler().GetTypesToDecompile(MainModule.Module).ToHashSet(); - MainModule.fileMap = GodotStuff.CreateFileMap(MainModule.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, excludeSubdirs, true); + var mainGodotClassHandles = GetGodotClassHandles(MainModule, typesToDecompile); + MainModule.fileMap = GodotStuff.CreateFileMap(MainModule.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, excludeSubdirs, true, mainGodotClassHandles); var additionalModuleCount = 0; - var fileToModuleMap = MainModule.fileMap.ToDictionary( - pair => pair.Key, - pair => MainModule,//.Module.FileName, - StringComparer.OrdinalIgnoreCase); // var moduleFileNameToMouduleMap = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var module in AdditionalModules) { // TODO: make CreateFileMap() work with multiple modules typesToDecompile = module.GetProjectDecompiler().GetTypesToDecompile(module.Module).ToHashSet(); + var moduleGodotClassHandles = GetGodotClassHandles(module, typesToDecompile); - var nfileMap = GodotStuff.CreateFileMap(module.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, null, true); + var nfileMap = GodotStuff.CreateFileMap(module.Module, typesToDecompile, this.originalProjectFiles, godot3xMetadata, null, true, moduleGodotClassHandles); additionalModuleCount += nfileMap.Count; string moduleName = module.Module.FileName; @@ -292,7 +303,14 @@ public GodotModuleDecompiler(string assemblyPath, string[]? originalProjectFiles { fixedPath = module.SubDirectory + "/" + pair.Key; } - module.fileMap.Add(fixedPath, pair.Value); + if (!module.fileMap.TryGetValue(fixedPath, out var existingHandles)) + { + module.fileMap.Add(fixedPath, pair.Value); + } + else + { + existingHandles.AddRange(pair.Value.Where(h => !existingHandles.Contains(h))); + } } if (module.fileMap.Count == 0) @@ -377,12 +395,19 @@ ProjectItem decompileFile(GodotModule module, string csprojPath) removeIfExists(csprojPath); ProjectId projectId; - var typesToExclude = excludeFiles?.Select(file => Common.TrimPrefix(file, "res://")).Where(module.fileMap.ContainsKey).Select(file => module.fileMap[file]).ToHashSet() ?? []; + var typesToExclude = excludeFiles?.Select(file => Common.TrimPrefix(file, "res://")) + .Where(module.fileMap.ContainsKey) + .SelectMany(file => module.fileMap[file]) + .ToHashSet() ?? []; + var handleToFileMap = module.fileMap + .SelectMany(pair => pair.Value.Select(h => new KeyValuePair(h, pair.Key))) + .GroupBy(p => p.Key) + .ToDictionary(group => group.Key, group => group.First().Value); using (var projectFileWriter = new StreamWriter(File.OpenWrite(csprojPath))) { projectId = godotProjectDecompiler.DecompileGodotProject( - module.Module, targetDirectory, projectFileWriter, typesToExclude, module.fileMap.ToDictionary(pair => pair.Value, pair => pair.Key), moduleToCsProjPath, module.depInfo, CustomVersionDetected, token); + module.Module, targetDirectory, projectFileWriter, typesToExclude, handleToFileMap, moduleToCsProjPath, module.depInfo, CustomVersionDetected, token); } ProjectItem item = new ProjectItem(csprojPath, projectId.PlatformName, projectId.Guid, projectId.TypeGuid); @@ -427,19 +452,19 @@ ProjectItem decompileFile(GodotModule module, string csprojPath) } public const string error_message = "// ERROR: Could not find file '{0}' in assembly '{1}.dll'."; - private (GodotModule?, TypeDefinitionHandle) GetScriptModuleAndType(string file) + private (GodotModule?, List) GetScriptModuleAndTypes(string file) { var path = Common.TrimPrefix(file, "res://"); if (!string.IsNullOrEmpty(path)) { - TypeDefinitionHandle foundType; + List? foundTypes; GodotModule? module = MainModule; - if (!module.fileMap.TryGetValue(path, out foundType)) + if (!module.fileMap.TryGetValue(path, out foundTypes)) { module = null; foreach (var m in AdditionalModules) { - if (m.fileMap.TryGetValue(path, out foundType)) + if (m.fileMap.TryGetValue(path, out foundTypes)) { module = m; break; @@ -447,17 +472,17 @@ ProjectItem decompileFile(GodotModule module, string csprojPath) } } - return (module, foundType); + return (module, foundTypes ?? []); } - return (null, default(TypeDefinitionHandle)); + return (null, []); } public string DecompileIndividualFile(string file) { var path = Common.TrimPrefix(file, "res://"); - var (module, type) = GetScriptModuleAndType(file); - if (module == null || type == default) + var (module, types) = GetScriptModuleAndTypes(file); + if (module == null || types.Count == 0) { return string.Format(error_message, file, MainModule.Name) + ( originalProjectFiles.Contains(path) @@ -466,8 +491,11 @@ public string DecompileIndividualFile(string file) ); } - var decompiler = module.CreateCSharpDecompilerWithPartials([type]); - return decompiler.DecompileTypesAsString([type]); + var decompiler = module.CreateCSharpDecompilerWithPartials(types); + var tree = decompiler.DecompileTypes(types); + var stringWriter = new StringWriter(); + tree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions)); + return stringWriter.ToString(); } private string GetPathForType(ITypeDefinition? typeDef){ @@ -475,11 +503,11 @@ private string GetPathForType(ITypeDefinition? typeDef){ return ""; } if (typeDef.ParentModule.AssemblyName == MainModule.Name){ - return MainModule.fileMap.FirstOrDefault(pair => pair.Value == (TypeDefinitionHandle)typeDef.MetadataToken).Key; + return MainModule.fileMap.FirstOrDefault(pair => pair.Value.Contains((TypeDefinitionHandle)typeDef.MetadataToken)).Key; } foreach (var module in AdditionalModules){ if (module.Name == typeDef.ParentModule.AssemblyName){ - return module.fileMap.FirstOrDefault(pair => pair.Value == (TypeDefinitionHandle)typeDef.MetadataToken).Key; + return module.fileMap.FirstOrDefault(pair => pair.Value.Contains((TypeDefinitionHandle)typeDef.MetadataToken)).Key; } } return ""; @@ -487,14 +515,23 @@ private string GetPathForType(ITypeDefinition? typeDef){ public GodotScriptInfo? GetScriptInfo(string file) { - var (module, type) = GetScriptModuleAndType(file); - if (module == null || type == default) + var (module, types) = GetScriptModuleAndTypes(file); + if (module == null || types.Count == 0) { return null; } var projectDecompiler = module.GetProjectDecompiler(); - var decompiler = module.CreateCSharpDecompilerWithPartials([type]); + var decompiler = module.CreateCSharpDecompilerWithPartials(types); + var type = types.FirstOrDefault(h => + { + var maybeTypeDef = decompiler.TypeSystem.MainModule.GetDefinition(h); + return maybeTypeDef != null && GodotStuff.IsGodotClass(maybeTypeDef); + }); + if (type == default) + { + return null; + } var typeDef = decompiler.TypeSystem.MainModule.GetDefinition(type); if (typeDef == null) @@ -636,8 +673,13 @@ private string GetPathForType(ITypeDefinition? typeDef){ iconPath = attr.FixedArguments[0].Value as string ?? ""; } } + // if we had more than one top-level-type in the file, we need to re-deccompile for all types + if (types.Count > 1) + { + syntaxTree = decompiler.DecompileTypes(types); + } StringWriter stringWriter = new StringWriter(); - syntaxTree.AcceptVisitor(new CSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(stringWriter, Settings.CSharpFormattingOptions)); var scriptText = stringWriter.ToString(); var scriptInfo = new GodotScriptInfo( diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj index 1dc943aa..3d3a570e 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj +++ b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecomp.csproj @@ -9,8 +9,8 @@ true - - + + diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs index 4f04907b..d54d9dbe 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotMonoDecompSettings.cs @@ -39,26 +39,45 @@ public class GodotMonoDecompSettings : DecompilerSettings /// public Version? GodotVersionOverride { get; set; } = null; - public GodotMonoDecompSettings() : base() + /// + /// Whether to remove the body of the generated JsonSourceGeneration context classes. + /// + public bool RemoveGeneratedJsonContextBody { get; set; } = false; + + /// + /// Whether to run LiftCollectionInitializers. + /// If false, the legacy RemoveBogusBaseConstructorCalls transform is used instead. + /// + public bool EnableCollectionInitializerLifting { get; set; } = true; + + private void InitializeDefaultSettings() { UseNestedDirectoriesForNamespaces = true; + // This avoids certain race conditions during static initialization when attempting to run the decompiled project. + AlwaysMoveInitializer = true; + } + + public GodotMonoDecompSettings() : base() + { + InitializeDefaultSettings(); } public GodotMonoDecompSettings(LanguageVersion languageVersion) : base(languageVersion) { - UseNestedDirectoriesForNamespaces = true; + InitializeDefaultSettings(); } public new GodotMonoDecompSettings Clone() { - var settings = (GodotMonoDecompSettings) base.Clone(); + var settings = (GodotMonoDecompSettings)base.Clone(); settings.WriteNuGetPackageReferences = WriteNuGetPackageReferences; settings.VerifyNuGetPackageIsFromNugetOrg = VerifyNuGetPackageIsFromNugetOrg; settings.CopyOutOfTreeReferences = CopyOutOfTreeReferences; settings.CreateAdditionalProjectsForProjectReferences = CreateAdditionalProjectsForProjectReferences; settings.OverrideLanguageVersion = OverrideLanguageVersion; settings.GodotVersionOverride = GodotVersionOverride; + settings.EnableCollectionInitializerLifting = EnableCollectionInitializerLifting; return settings; } diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs b/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs index 6057d4e2..3b0787d6 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotProjectDecompiler.cs @@ -308,7 +308,21 @@ public virtual CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts) decompiler.AstTransforms.Add(new RemoveGodotScriptPathAttribute()); decompiler.AstTransforms.Add(new RemoveAutoAccessor()); decompiler.AstTransforms.Add(new GodotMonoDecomp.RemoveEmbeddedAttributes()); + decompiler.AstTransforms.Add(new RestoreGeneratedRegexMethods()); decompiler.AstTransforms.Add(new RemoveGeneratedExceptionThrows()); + if (Settings.EnableCollectionInitializerLifting) + { + decompiler.AstTransforms.Add(new LiftCollectionInitializers()); + } + else + { + decompiler.AstTransforms.Add(new RemoveBogusBaseConstructorCalls()); + } + decompiler.AstTransforms.Add(new FixSwitchExpressionCasts()); + if (Settings.RemoveGeneratedJsonContextBody) + { + decompiler.AstTransforms.Add(new RemoveJsonSourceGenerationClassBody()); + } if (Settings.GodotVersionOverride?.Major == 3) { decompiler.AstTransforms.Add(new RemoveMathF3x()); @@ -329,7 +343,7 @@ IEnumerable WriteAssemblyInfo(DecompilerTypeSystem ts, Cancella string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs"); using (var w = CreateFile(Path.Combine(TargetDirectory, assemblyInfo))) { - syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); } return new[] { new ProjectItemInfo("Compile", assemblyInfo) }; } @@ -455,7 +469,7 @@ void ProcessFiles(List> files) var path = Path.Combine(TargetDirectory, file.Key); using StreamWriter w = new StreamWriter(path); - syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new GodotCSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); } catch (Exception innerException) when (!(innerException is OperationCanceledException || innerException is DecompilerException)) { diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs b/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs index 7f93e3de..4cd304e8 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs @@ -336,16 +336,24 @@ public static Dictionary> DeduceParentNamespaceDirectori } - public static Dictionary CreateFileMap(MetadataFile module, + public static Dictionary> CreateFileMap(MetadataFile module, IEnumerable typesToDecompile, List filesInOriginal, Dictionary? scriptMetadata, IEnumerable? excludedSubdirectories, - bool useNestedDirectoriesForNamespaces) + bool useNestedDirectoriesForNamespaces, + ISet? godotClassHandles = null) { - var fileMap = new Dictionary(); + var fileMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); + godotClassHandles ??= new HashSet(); + var canonicalPaths = new HashSet(); + var canonicalHandles = new HashSet(); var metadata = module.Metadata; Dictionary? metadataFQNToFileMap = null; + // look at the files in the original project and find a common root for all the files + // var allFiles = filesInOriginal.Select(f => Path.GetDirectoryName(f) ?? "").Where(d => !string.IsNullOrEmpty(d) && !d.StartsWith("addons", StringComparison.OrdinalIgnoreCase)).ToHashSet(); + string? globalCommonRoot = null;//Common.FindCommonRoot(allFiles) ?? null; + if (scriptMetadata != null) { // create a map of metadata FQN to file path @@ -402,6 +410,99 @@ bool IsInExcludedSubdir(string? file) return IsExcludedSubdir(Path.GetDirectoryName(file)); } + void AddHandleToPath(string path, TypeDefinitionHandle h) + { + path = _NormalizePath(path); + if (!fileMap.TryGetValue(path, out var handles)) + { + handles = []; + fileMap[path] = handles; + } + if (!handles.Contains(h)) + { + handles.Add(h); + } + } + + string RelocateByPrefixingUnderscoreDirectory(string path) + { + path = _NormalizePath(path); + var fileName = Path.GetFileName(path); + var dir = Path.GetDirectoryName(path); + // dir = string.IsNullOrEmpty(dir) ? "_" : "_" + _NormalizePath(dir); + dir = string.IsNullOrEmpty(dir) ? "_collision" : _PathCombine(dir, "_collision"); + + return _PathCombine(dir, fileName); + } + + void RelocateExistingGlobalHandle(string currentPath, TypeDefinitionHandle existingGlobalHandle) + { + currentPath = _NormalizePath(currentPath); + if (!fileMap.TryGetValue(currentPath, out var currentHandles)) + { + return; + } + if (!currentHandles.Remove(existingGlobalHandle)) + { + return; + } + if (currentHandles.Count == 0) + { + fileMap.Remove(currentPath); + } + var relocatedPath = RelocateByPrefixingUnderscoreDirectory(currentPath); + PlaceHandleAtPath(relocatedPath, existingGlobalHandle); + } + + void PlaceHandleAtPath(string path, TypeDefinitionHandle h) + { + path = _NormalizePath(path); + if (!godotClassHandles.Contains(h)) + { + AddHandleToPath(path, h); + return; + } + + while (true) + { + var existingHandles = fileMap.TryGetValue(path, out var hs) ? hs : []; + var existingGlobals = existingHandles.Where(godotClassHandles.Contains).ToList(); + + if (existingGlobals.Count == 0) + { + AddHandleToPath(path, h); + return; + } + + bool incomingIsCanonical = canonicalHandles.Contains(h); + bool anyExistingCanonical = existingGlobals.Any(canonicalHandles.Contains); + if (incomingIsCanonical && anyExistingCanonical) + { + Console.Error.WriteLine( + $"Multiple canonical Godot class types map to '{path}'. Canonical collisions are not allowed."); + // just add it anyway + AddHandleToPath(path, h); + return; + } + if (!incomingIsCanonical && anyExistingCanonical) + { + path = RelocateByPrefixingUnderscoreDirectory(path); + continue; + } + if (incomingIsCanonical) + { + foreach (var existingGlobal in existingGlobals.Where(g => !canonicalHandles.Contains(g)).ToList()) + { + RelocateExistingGlobalHandle(path, existingGlobal); + } + AddHandleToPath(path, h); + return; + } + + path = RelocateByPrefixingUnderscoreDirectory(path); + } + } + var processAgain = new HashSet(); var namespaceToDirectory = new Dictionary>(); void addToNamespaceToFile(string ns, string file) @@ -420,6 +521,15 @@ void addToNamespaceToFile(string ns, string file) namespaceToDirectory[ns] = new HashSet { dir }; } } + void addToCanonicalPaths(string path, TypeDefinitionHandle h, TypeDefinition type) + { + path = _NormalizePath(path); + canonicalPaths.Add(path); + canonicalHandles.Add(h); + addToNamespaceToFile(metadata.GetString(type.Namespace), path); + PlaceHandleAtPath(path, h); + } + foreach (var h in typesToDecompile) { var type = metadata.GetTypeDefinition(h); @@ -429,8 +539,7 @@ void addToNamespaceToFile(string ns, string file) // that the file is referenced by other files in the project, so the path MUST match. if (!string.IsNullOrEmpty(scriptPath)) { - addToNamespaceToFile(metadata.GetString(type.Namespace),scriptPath); - fileMap[scriptPath] = h; + addToCanonicalPaths(scriptPath, h, type); } else { @@ -442,17 +551,21 @@ void addToNamespaceToFile(string ns, string file) if (metadataFQNToFileMap.TryGetValue(fqn, out var filePath)) { filePath = Common.TrimPrefix(filePath, "res://"); - addToNamespaceToFile(metadata.GetString(type.Namespace), filePath); - fileMap[filePath] = h; + addToCanonicalPaths(filePath, h, type); continue; } } processAgain.Add(h); } } - namespaceToDirectory = DeduceParentNamespaceDirectories(namespaceToDirectory); - string default_dir = "src"; + // 3.x games have much less canonical file paths than 4.x games, so do it after `GetPathFromOriginalFiles` step + if (scriptMetadata == null) + { + namespaceToDirectory = DeduceParentNamespaceDirectories(namespaceToDirectory); + } + + string default_dir = !string.IsNullOrEmpty(globalCommonRoot) ? globalCommonRoot : "src"; while (IsExcludedSubdir(default_dir)){ default_dir = "_" + default_dir; } @@ -471,6 +584,10 @@ string GetAutoFileNameForHandle(TypeDefinitionHandle h) else { string dir = useNestedDirectoriesForNamespaces ? GodotProjectDecompiler.CleanUpPath(ns) : GodotProjectDecompiler.CleanUpDirectoryName(ns); + if (!string.IsNullOrEmpty(globalCommonRoot) && !dir.StartsWith(globalCommonRoot, StringComparison.OrdinalIgnoreCase)) + { + dir = _PathCombine(globalCommonRoot, dir); + } // ensure dir separator is '/' dir = dir.Replace(Path.DirectorySeparatorChar, '/'); // TODO: come back to this @@ -488,7 +605,6 @@ string GetPathFromOriginalFiles(string file_path) string scriptPath = ""; // empty vector of strings var possibles = filesInOriginal.Where(f => - !fileMap.ContainsKey(f) && Path.GetFileName(f) == Path.GetFileName(file_path) && !IsInExcludedSubdir(f) ) @@ -497,7 +613,6 @@ string GetPathFromOriginalFiles(string file_path) if (possibles.Count == 0) { possibles = filesInOriginal.Where(f => - !fileMap.ContainsKey(f) && Path.GetFileName(f).ToLower() == Path.GetFileName(file_path).ToLower() && !IsInExcludedSubdir(f) ).ToList(); @@ -552,19 +667,21 @@ string GetPathFromOriginalFiles(string file_path) foreach (var pair in potentialMap) { - if (pair.Value.Count == 1) + foreach (var h in pair.Value) { - var type = metadata.GetTypeDefinition(pair.Value[0]); + var type = metadata.GetTypeDefinition(h); addToNamespaceToFile(metadata.GetString(type.Namespace), pair.Key); - fileMap[pair.Key] = pair.Value[0]; - } else { - foreach (var h in pair.Value) - { - processAgainAgain.Add(h); - } + PlaceHandleAtPath(pair.Key, h); } } + if (scriptMetadata != null) + { + namespaceToDirectory = DeduceParentNamespaceDirectories(namespaceToDirectory); + } + + + HashSet GetNamespaceDirectories(string ns) { @@ -577,20 +694,6 @@ HashSet GetNamespaceDirectories(string ns) : []; } - string AppendNumberToFile(string path){ - var fileName = Path.GetFileName(path); - var fileStem = Path.GetFileNameWithoutExtension(fileName); - var fileExtension = Path.GetExtension(fileName); - var parentDir = Path.GetDirectoryName(path) ?? ""; - var number = 1; - while (fileMap.ContainsKey(path)) - { - path = _PathCombine(Path.GetDirectoryName(path) ?? "", fileStem + number.ToString() + fileExtension); - number++; - } - return path; - } - foreach (var h in processAgainAgain) { var type = metadata.GetTypeDefinition(h); @@ -651,12 +754,8 @@ string AppendNumberToFile(string path){ p = _PathCombine(default_dir, p); } } - if (fileMap.ContainsKey(p)) - { - p = AppendNumberToFile(p); - } p = _NormalizePath(p); - fileMap[p] = h; + PlaceHandleAtPath(p, h); } foreach (var h in dupes) @@ -664,7 +763,7 @@ string AppendNumberToFile(string path){ var auto_path = GetAutoFileNameForHandle(h); var scriptPath = GetPathFromOriginalFiles(auto_path); - if (scriptPath == "" || scriptPath == "" || fileMap.ContainsKey(scriptPath)) + if (scriptPath == "" || scriptPath == "") { scriptPath = auto_path; } @@ -673,17 +772,40 @@ string AppendNumberToFile(string path){ { scriptPath = _PathCombine(default_dir, scriptPath); } - if (fileMap.ContainsKey(scriptPath)) + PlaceHandleAtPath(scriptPath, h); + } + var caselessDict = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var pair in fileMap) + { + var isGodotClass = pair.Value.Any(h => godotClassHandles.Contains(h)); + + var key = pair.Key; + var existingHandles = caselessDict.TryGetValue(key, out var hs) ? hs : []; + if (existingHandles.Count > 0) { - scriptPath = AppendNumberToFile(scriptPath); - } + var caselessPair = caselessDict.First(p => p.Key.Equals(pair.Key, StringComparison.OrdinalIgnoreCase)); - fileMap[scriptPath] = h; + if (canonicalPaths.Contains(caselessPair.Key)) + { + key = caselessPair.Key; + } + else if (!canonicalPaths.Contains(pair.Key)) // else if the current key is NOT in the canonical paths... + { + if (caselessPair.Key.Count(char.IsUpper) > pair.Key.Count(char.IsUpper)) + { + key = caselessPair.Key; + } + } + if (key != caselessPair.Key) + { + // remove the current key and then add it back below, so that we replace any key collisions + caselessDict.Remove(caselessPair.Key); + } + } + var newHandles = existingHandles.Concat(pair.Value).Distinct().ToList(); + caselessDict[key] = newHandles; } - return fileMap.ToDictionary( - pair => pair.Key, - pair => pair.Value, - StringComparer.OrdinalIgnoreCase); + return caselessDict; } diff --git a/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs b/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs new file mode 100644 index 00000000..281f7fff --- /dev/null +++ b/godot-mono-decomp/GodotMonoDecomp/LiftCollectionInitializers.cs @@ -0,0 +1,1914 @@ +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; + +namespace GodotMonoDecomp; + +/// +/// Lifts a narrow set of constructor prelude initializers back to declaration initializers. +/// This targets list-prelude patterns that ILSpy lowers into SetCount/AsSpan/indexed writes. +/// +public class LiftCollectionInitializers : DepthFirstAstVisitor, IAstTransform +{ + private TypeDeclaration? currentType; + + public void Run(AstNode rootNode, TransformContext context) + { + rootNode.AcceptVisitor(this); + } + + public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration) + { + var previous = currentType; + currentType = typeDeclaration; + try + { + base.VisitTypeDeclaration(typeDeclaration); + } + finally + { + currentType = previous; + } + } + + public List CollectMembersWithInitializers(TypeDeclaration typeDeclaration) + { + var participatingMembers = new List(); + foreach (var member in typeDeclaration.Members) + { + if (member is FieldDeclaration field) + { + foreach (var variable in field.Variables) + { + if (!variable.AssignToken.IsNull || !variable.Initializer.IsNull) + { + foreach (var v in field.Variables) + { + participatingMembers.Add(variable.Name); + } + break; + } + } + } + else if (member is PropertyDeclaration property) + { + if (!property.Initializer.IsNull || !property.AssignToken.IsNull) + { + participatingMembers.Add(property.Name); + } + } + } + return participatingMembers.Distinct().ToList(); + } + + + public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration) + { + if (currentType == null || constructorDeclaration.Body.IsNull) + { + base.VisitConstructorDeclaration(constructorDeclaration); + return; + } + + bool isStaticConstructor = (constructorDeclaration.Modifiers & Modifiers.Static) == Modifiers.Static; + var statements = constructorDeclaration.Body.Statements.ToArray(); + int boundaryIndex = isStaticConstructor ? statements.Length : FindConstructorBoundaryIndex(statements); + if (!isStaticConstructor && boundaryIndex < 0) + { + base.VisitConstructorDeclaration(constructorDeclaration); + return; + } + + var memberMap = BuildMemberMap(currentType); + var recoveredMemberOrder = CollectMembersWithInitializers(currentType); + var matchedListInitByLocal = new Dictionary(StringComparer.Ordinal); + var statementsToRemove = new HashSet(); + var localNames = CollectConstructorLocalNames(statements, boundaryIndex); + + int i = 0; + while (i < boundaryIndex) + { + if (TryMatchListPrelude(statements, i, boundaryIndex, out var listMatch)) + { + if (TryApplyInitializer(memberMap, listMatch.TargetMemberName, listMatch.InitializerExpression, isStaticConstructor)) + { + recoveredMemberOrder.Add(listMatch.TargetMemberName); + matchedListInitByLocal[listMatch.ListVariableName] = listMatch.InitializerExpression; + foreach (var statement in listMatch.MatchedStatements) + { + statementsToRemove.Add(statement); + } + } + + i = listMatch.NextIndex; + continue; + } + + if (TryMatchListAddRangeSpreadPrelude(statements, i, boundaryIndex, out var listSpreadMatch)) + { + if (TryApplyInitializer(memberMap, listSpreadMatch.TargetMemberName, listSpreadMatch.InitializerExpression, isStaticConstructor)) + { + recoveredMemberOrder.Add(listSpreadMatch.TargetMemberName); + matchedListInitByLocal[listSpreadMatch.ListVariableName] = listSpreadMatch.InitializerExpression; + foreach (var statement in listSpreadMatch.MatchedStatements) + { + statementsToRemove.Add(statement); + } + } + + i = listSpreadMatch.NextIndex; + continue; + } + + if (TryMatchHashSetForeachSpreadPrelude(statements, i, boundaryIndex, out var hashSetSpreadMatch)) + { + if (TryApplyInitializer(memberMap, hashSetSpreadMatch.TargetMemberName, hashSetSpreadMatch.InitializerExpression, isStaticConstructor)) + { + recoveredMemberOrder.Add(hashSetSpreadMatch.TargetMemberName); + matchedListInitByLocal[hashSetSpreadMatch.ListVariableName] = hashSetSpreadMatch.InitializerExpression; + foreach (var statement in hashSetSpreadMatch.MatchedStatements) + { + statementsToRemove.Add(statement); + } + } + + i = hashSetSpreadMatch.NextIndex; + continue; + } + + if (TryMatchListSpreadBuilderWrapperAssignment(statements, i, boundaryIndex, currentType, out var wrappedSpreadMatch)) + { + if (!ReferencesCtorLocal(wrappedSpreadMatch.InitializerExpression, localNames) + && TryApplyInitializer(memberMap, wrappedSpreadMatch.TargetMemberName, wrappedSpreadMatch.InitializerExpression, isStaticConstructor)) + { + recoveredMemberOrder.Add(wrappedSpreadMatch.TargetMemberName); + matchedListInitByLocal[wrappedSpreadMatch.ListVariableName] = wrappedSpreadMatch.InitializerExpression; + foreach (var statement in wrappedSpreadMatch.MatchedStatements) + { + statementsToRemove.Add(statement); + } + } + + i = wrappedSpreadMatch.NextIndex; + continue; + } + + if (TryMatchConditionalTempAssignment(statements, i, boundaryIndex, currentType, out var conditionalTempMatch)) + { + if (!ReferencesCtorLocal(conditionalTempMatch.InitializerExpression, localNames) + && TryApplyInitializer(memberMap, conditionalTempMatch.TargetMemberName, conditionalTempMatch.InitializerExpression, isStaticConstructor)) + { + recoveredMemberOrder.Add(conditionalTempMatch.TargetMemberName); + foreach (var statement in conditionalTempMatch.MatchedStatements) + { + statementsToRemove.Add(statement); + } + } + + i = conditionalTempMatch.NextIndex; + continue; + } + + if (TryMatchSimpleAssignment(statements[i], currentType, out var simpleTarget, out var simpleInitializer)) + { + var candidateInitializer = simpleInitializer; + if (ReferencesCtorLocal(candidateInitializer, localNames) + && TryRewriteInitializerWithRecoveredLocals(candidateInitializer, matchedListInitByLocal, out var rewrittenInitializer)) + { + candidateInitializer = rewrittenInitializer; + } + candidateInitializer = SimplifyRedundantReadOnlyListCollectionWrappers(candidateInitializer); + + if (!ReferencesCtorLocal(candidateInitializer, localNames) + && TryApplyInitializer(memberMap, simpleTarget, candidateInitializer, isStaticConstructor)) + { + recoveredMemberOrder.Add(simpleTarget); + statementsToRemove.Add(statements[i]); + } + i++; + continue; + } + + // check if this is an assignment to a member at all; if so, break. We do not want to lift any more lest we change the ordering of the initializers + if (isStaticConstructor || boundaryIndex < 0) + { + if (statements[i] is ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment }) + { + if (TryGetAssignedMemberName(assignment.Left, out var memberName) && memberMap.ContainsKey(memberName)) + { + break; + } + } + } + + i++; + } + + if (!isStaticConstructor) + { + Statement? boundaryStatement = statements[boundaryIndex]; + if (TryBuildConstructorInitializer( + constructorDeclaration, + statements, + boundaryIndex, + boundaryStatement, + matchedListInitByLocal, + out var constructorInitializer, + out var ctorPreludeStatements)) + { + constructorDeclaration.Initializer = constructorInitializer; + statementsToRemove.Add(boundaryStatement); + foreach (var statement in ctorPreludeStatements) + { + statementsToRemove.Add(statement); + } + } + } + + foreach (var statement in statements.Reverse()) + { + if (statementsToRemove.Contains(statement)) + { + statement.Remove(); + } + } + + // Only run temp-noise cleanup when this transform actually consumed generated prelude statements. + if (statementsToRemove.Count > 0) + { + CleanupLeadingTempNoise(constructorDeclaration); + } + + // If lifting consumed generated ctor statements and left no body/initializer content, + // drop the constructor declaration entirely. + if (statementsToRemove.Count > 0 + && constructorDeclaration.Body.Statements.Count == 0 + && constructorDeclaration.Initializer.IsNull) + { + constructorDeclaration.Remove(); + ReorderRecoveredMembers(currentType, recoveredMemberOrder, memberMap); + return; + } + + ReorderRecoveredMembers(currentType, recoveredMemberOrder, memberMap); + } + + private static int FindConstructorBoundaryIndex(IReadOnlyList statements) + { + for (int i = 0; i < statements.Count; i++) + { + if (TryGetCtorInvocation(statements[i], out _, out _)) + { + return i; + } + } + + return -1; + } + + private static bool TryGetCtorInvocation( + Statement statement, + out ConstructorInitializerType initializerType, + out InvocationExpression invocation) + { + initializerType = ConstructorInitializerType.Any; + invocation = null!; + if (statement is not ExpressionStatement { Expression: InvocationExpression invocationExpression }) + { + return false; + } + + if (invocationExpression.Target is not MemberReferenceExpression memberReference + || !string.Equals(memberReference.MemberName, "_002Ector", StringComparison.Ordinal)) + { + return false; + } + + if (memberReference.Target is BaseReferenceExpression) + { + initializerType = ConstructorInitializerType.Base; + } + else if (memberReference.Target is ThisReferenceExpression) + { + initializerType = ConstructorInitializerType.This; + } + else + { + return false; + } + + invocation = invocationExpression; + return true; + } + + private static Dictionary BuildMemberMap(TypeDeclaration typeDeclaration) + { + var map = new Dictionary(StringComparer.Ordinal); + foreach (var member in typeDeclaration.Members) + { + if (member is FieldDeclaration field) + { + foreach (var variable in field.Variables) + { + map.TryAdd(variable.Name, field); + } + } + else if (member is PropertyDeclaration property) + { + map.TryAdd(property.Name, property); + } + } + + return map; + } + + private static bool TryApplyInitializer( + Dictionary memberMap, + string memberName, + Expression initializer, + bool expectStaticMember) + { + if (!memberMap.TryGetValue(memberName, out var member)) + { + return false; + } + if (IsStaticMember(member) != expectStaticMember) + { + return false; + } + + switch (member) + { + case FieldDeclaration field: + var variable = field.Variables.FirstOrDefault(v => string.Equals(v.Name, memberName, StringComparison.Ordinal)); + if (variable == null) + { + return false; + } + variable.Initializer = initializer.Clone(); + return true; + case PropertyDeclaration property: + if (!property.IsAutomaticProperty) + { + return false; + } + property.Initializer = initializer.Clone(); + return true; + default: + return false; + } + } + + private static bool IsStaticMember(EntityDeclaration member) + { + return (member.Modifiers & Modifiers.Static) == Modifiers.Static; + } + + private static HashSet CollectConstructorLocalNames(IReadOnlyList statements, int boundaryIndex) + { + var localNames = new HashSet(StringComparer.Ordinal); + for (int i = 0; i < boundaryIndex && i < statements.Count; i++) + { + if (statements[i] is not VariableDeclarationStatement declaration) + { + continue; + } + + foreach (var variable in declaration.Variables) + { + if (!string.IsNullOrEmpty(variable.Name)) + { + localNames.Add(variable.Name); + } + } + } + + return localNames; + } + + private static bool ReferencesCtorLocal(Expression expression, HashSet localNames) + { + if (localNames.Count == 0) + { + return false; + } + + if (expression is IdentifierExpression { Identifier: var identifier } + && localNames.Contains(identifier)) + { + return true; + } + + return expression.Descendants.OfType() + .Any(id => localNames.Contains(id.Identifier)); + } + + private static bool TryMatchSimpleAssignment(Statement statement, TypeDeclaration currentType, out string memberName, out Expression initializer) + { + memberName = string.Empty; + initializer = Expression.Null; + if (statement is not ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment }) + { + return false; + } + + if (!TryGetAssignedMemberName(assignment.Left, out memberName)) + { + return false; + } + + // Keep conservative: only side-effect free literal/create/delegate expressions. + if (IsAllowedLiftInitializerExpression(assignment.Right, currentType)) + { + initializer = assignment.Right; + return true; + } + + return false; + } + + private static bool TryMatchConditionalTempAssignment( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + TypeDeclaration currentType, + out ConditionalTempAssignmentMatch match) + { + match = default; + if (startIndex + 2 >= boundaryIndex + || statements[startIndex] is not VariableDeclarationStatement tempDecl + || tempDecl.Variables.Count != 1 + || tempDecl.Variables.FirstOrDefault() is not { } tempVar + || !tempVar.Initializer.IsNull) + { + return false; + } + + string tempVarName = tempVar.Name; + if (string.IsNullOrEmpty(tempVarName) + || statements[startIndex + 1] is not IfElseStatement ifElse + || ifElse.TrueStatement is not BlockStatement trueBlock + || ifElse.FalseStatement is not BlockStatement falseBlock + || !TryMatchConditionalTempBranch(trueBlock, tempVarName, currentType, out var trueInitializer) + || !TryMatchConditionalTempBranch(falseBlock, tempVarName, currentType, out var falseInitializer)) + { + return false; + } + + if (!TryMatchTerminalAssignment(statements[startIndex + 2], tempVarName, out var memberName)) + { + return false; + } + + var conditionalInitializer = new ConditionalExpression( + ifElse.Condition.Clone(), + trueInitializer.Clone(), + falseInitializer.Clone()); + match = new ConditionalTempAssignmentMatch( + memberName, + conditionalInitializer, + new List { statements[startIndex], statements[startIndex + 1], statements[startIndex + 2] }, + startIndex + 3 + ); + return true; + } + + private static bool TryMatchConditionalTempBranch( + BlockStatement branchBlock, + string tempVarName, + TypeDeclaration currentType, + out Expression branchInitializer) + { + branchInitializer = Expression.Null; + if (branchBlock.Statements.Count != 2 + || branchBlock.Statements.FirstOrDefault() is not VariableDeclarationStatement localDecl + || localDecl.Variables.Count != 1 + || localDecl.Variables.FirstOrDefault() is not { } localVar + || localVar.Initializer is not { } localInitializer + || localInitializer.IsNull + || !IsAllowedLiftInitializerExpression(localInitializer, currentType) + || branchBlock.Statements.LastOrDefault() is not ExpressionStatement + { + Expression: AssignmentExpression + { + Operator: AssignmentOperatorType.Assign, + Left: IdentifierExpression { Identifier: var assignedTempVarName }, + Right: IdentifierExpression { Identifier: var assignedLocalName } + } + } + || !string.Equals(assignedTempVarName, tempVarName, StringComparison.Ordinal) + || !string.Equals(assignedLocalName, localVar.Name, StringComparison.Ordinal)) + { + return false; + } + + branchInitializer = localInitializer.Clone(); + return true; + } + + private static bool TryRewriteInitializerWithRecoveredLocals( + Expression initializer, + IReadOnlyDictionary recoveredInitializersByLocal, + out Expression rewritten) + { + rewritten = initializer; + if (recoveredInitializersByLocal.Count == 0) + { + return false; + } + + var clone = initializer.Clone(); + bool changed = false; + if (clone is IdentifierExpression { Identifier: var rootIdentifier } + && recoveredInitializersByLocal.TryGetValue(rootIdentifier, out var rootReplacement)) + { + clone = rootReplacement.Clone(); + changed = true; + } + + foreach (var identifier in clone.Descendants.OfType().ToArray()) + { + if (!recoveredInitializersByLocal.TryGetValue(identifier.Identifier, out var replacement)) + { + continue; + } + + identifier.ReplaceWith(replacement.Clone()); + changed = true; + } + + if (!changed) + { + return false; + } + + rewritten = clone; + return true; + } + + private static Expression SimplifyRedundantReadOnlyListCollectionWrappers(Expression initializer) + { + Expression expression = initializer.Clone(); + + while (expression is ObjectCreateExpression rootCreate + && TryUnwrapReadOnlyListOfListCollection(rootCreate, out var rootReplacement)) + { + expression = rootReplacement.Clone(); + } + + foreach (var objectCreate in expression.Descendants.OfType().ToArray()) + { + if (TryUnwrapReadOnlyListOfListCollection(objectCreate, out var replacement)) + { + objectCreate.ReplaceWith(replacement.Clone()); + } + } + + return expression; + } + + private static bool TryUnwrapReadOnlyListOfListCollection(ObjectCreateExpression readOnlyListCreate, out Expression collectionExpression) + { + collectionExpression = Expression.Null; + if (!IsTypeIdentifier(readOnlyListCreate.Type, "_003C_003Ez__ReadOnlyList") + || readOnlyListCreate.Arguments.Count != 1 + || readOnlyListCreate.Arguments.FirstOrDefault() is not ObjectCreateExpression listCreate + || !IsTypeIdentifier(listCreate.Type, "List") + || listCreate.Arguments.Count != 1 + || listCreate.Arguments.FirstOrDefault() is not ArrayInitializerExpression arrayInitializer + || arrayInitializer.Annotation() == null) + { + return false; + } + + collectionExpression = arrayInitializer.Clone(); + return true; + } + + private static bool IsTypeIdentifier(AstType type, string identifier) + { + return type is SimpleType { Identifier: var typeIdentifier } + && string.Equals(typeIdentifier, identifier, StringComparison.Ordinal); + } + + private static bool IsAllowedMemberReferenceOrInvocation(Expression expression, TypeDeclaration currentType) + { + if (expression is not MemberReferenceExpression && expression is not InvocationExpression) + { + return false; + } + + var typeMemberNames = CollectTypeMemberNames(currentType); + if (ReferencesCurrentTypeMembers(expression, currentType, typeMemberNames)) + { + return false; + } + + // Unqualified invocation can bind to a ctor-local function or type method; keep this conservative. + if (expression is InvocationExpression { Target: IdentifierExpression }) + { + return false; + } + + return true; + } + + private static HashSet CollectTypeMemberNames(TypeDeclaration currentType) + { + var names = new HashSet(StringComparer.Ordinal); + foreach (var member in currentType.Members) + { + switch (member) + { + case FieldDeclaration field: + foreach (var variable in field.Variables) + { + if (!string.IsNullOrEmpty(variable.Name)) + { + names.Add(variable.Name); + } + } + break; + case PropertyDeclaration property: + if (!string.IsNullOrEmpty(property.Name)) + { + names.Add(property.Name); + } + break; + case MethodDeclaration method: + if (!string.IsNullOrEmpty(method.Name)) + { + names.Add(method.Name); + } + break; + case TypeDeclaration nestedType: + if (!string.IsNullOrEmpty(nestedType.Name)) + { + names.Add(nestedType.Name); + } + break; + } + } + + return names; + } + + private static bool ReferencesCurrentTypeMembers(Expression expression, TypeDeclaration currentType, HashSet typeMemberNames) + { + if (expression is ThisReferenceExpression + || expression is BaseReferenceExpression) + { + return true; + } + + if (expression is IdentifierExpression { Identifier: var identifier } + && typeMemberNames.Contains(identifier)) + { + return true; + } + + if (expression is MemberReferenceExpression memberReference + && memberReference.Target is IdentifierExpression { Identifier: var targetIdentifier } + && string.Equals(targetIdentifier, currentType.Name, StringComparison.Ordinal)) + { + return true; + } + + if (expression is MemberReferenceExpression + { + Target: TypeReferenceExpression + { + Type: SimpleType { Identifier: var targetTypeIdentifier } + } + } + && string.Equals(targetTypeIdentifier, currentType.Name, StringComparison.Ordinal)) + { + return true; + } + + if (expression.Descendants.OfType().Any() + || expression.Descendants.OfType().Any()) + { + return true; + } + + if (expression.Descendants.OfType() + .Any(id => typeMemberNames.Contains(id.Identifier))) + { + return true; + } + + if (expression.Descendants.OfType() + .Any(member => member.Target is IdentifierExpression { Identifier: var id } + && string.Equals(id, currentType.Name, StringComparison.Ordinal))) + { + return true; + } + + return expression.Descendants.OfType() + .Any(member => member.Target is TypeReferenceExpression { Type: SimpleType { Identifier: var id } } + && string.Equals(id, currentType.Name, StringComparison.Ordinal)); + } + + private static bool IsAllowedLiftInitializerExpression(Expression expression, TypeDeclaration currentType) + { + return IsAllowedSimpleInitializerExpression(expression) + || IsAllowedMemberReferenceOrInvocation(expression, currentType); + } + + private static bool IsAllowedSimpleInitializerExpression(Expression expression) + { + return expression is PrimitiveExpression + || expression is NullReferenceExpression + || expression is ObjectCreateExpression + || expression is LambdaExpression + || expression is AnonymousMethodExpression; + } + + private static bool TryGetAssignedMemberName(Expression left, out string memberName) + { + memberName = string.Empty; + if (left is IdentifierExpression ident) + { + memberName = ident.Identifier; + return true; + } + + if (left is MemberReferenceExpression memberRef + && memberRef.Target is ThisReferenceExpression or IdentifierExpression) + { + memberName = memberRef.MemberName; + return true; + } + + return false; + } + + private static bool TryMatchListPrelude( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + out ListPreludeMatch match) + { + match = default; + if (startIndex + 3 >= boundaryIndex) + { + return false; + } + + if (statements[startIndex] is not VariableDeclarationStatement listDecl + || listDecl.Variables.Count != 1 + || listDecl.Variables.FirstOrDefault() is not { } listVariable + || listVariable.Initializer is not ObjectCreateExpression listCreate) + { + return false; + } + + string listVarName = listVariable.Name; + if (string.IsNullOrEmpty(listVarName)) + { + return false; + } + + if (!IsCollectionsMarshalCall(statements[startIndex + 1], "SetCount", listVarName)) + { + return false; + } + + if (statements[startIndex + 2] is not VariableDeclarationStatement spanDecl + || spanDecl.Variables.Count != 1 + || spanDecl.Variables.FirstOrDefault() is not { } spanVariable + || spanVariable.Initializer is not InvocationExpression spanInit + || !IsCollectionsMarshalAsSpanCall(spanInit, listVarName)) + { + return false; + } + + string spanVarName = spanVariable.Name; + var values = new List(); + var matchedStatements = new List { statements[startIndex], statements[startIndex + 1], statements[startIndex + 2] }; + string targetMemberName = string.Empty; + int i = startIndex + 3; + for (; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryMatchTerminalAssignment(statement, listVarName, out targetMemberName)) + { + matchedStatements.Add(statement); + break; + } + + if (TryCollectSpanAssignmentValue(statement, spanVarName, out var value)) + { + values.Add(value.Clone()); + matchedStatements.Add(statement); + continue; + } + + if (TryCollectNestedSpanAssignmentValue( + statements, + i, + boundaryIndex, + spanVarName, + out var nestedValue, + out var nextIndex, + out var nestedMatchedStatements)) + { + values.Add(nestedValue.Clone()); + matchedStatements.AddRange(nestedMatchedStatements); + i = nextIndex - 1; + continue; + } + + if (IsAllowedPreludeNoise(statement)) + { + continue; + } + + return false; + } + + if (i >= boundaryIndex || string.IsNullOrEmpty(targetMemberName)) + { + return false; + } + + var collectionInitializer = new ObjectCreateExpression(listDecl.Type.Clone()) + { + Initializer = new ArrayInitializerExpression(values) + }; + match = new ListPreludeMatch( + listVarName, + targetMemberName, + collectionInitializer, + matchedStatements, + i + 1 + ); + return true; + } + + private static bool TryMatchListAddRangeSpreadPrelude( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + out ListPreludeMatch match) + { + match = default; + if (startIndex + 2 >= boundaryIndex) + { + return false; + } + + if (statements[startIndex] is not VariableDeclarationStatement listDecl + || listDecl.Variables.Count != 1 + || listDecl.Variables.FirstOrDefault() is not { } listVariable + || listVariable.Initializer is not ObjectCreateExpression listCreate + || !IsGenericCollectionType(listCreate.Type, "List")) + { + return false; + } + + string listVarName = listVariable.Name; + if (string.IsNullOrEmpty(listVarName)) + { + return false; + } + + var segments = new List(); + var matchedStatements = new List { statements[startIndex] }; + string targetMemberName = string.Empty; + int i = startIndex + 1; + for (; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryMatchTerminalAssignment(statement, listVarName, out targetMemberName)) + { + matchedStatements.Add(statement); + break; + } + + if (TryMatchCollectionSpreadOperation(statement, listVarName, "AddRange", out var spreadSource)) + { + segments.Add(SpreadSegment.FromRange(spreadSource.Clone())); + matchedStatements.Add(statement); + continue; + } + + if (TryMatchCollectionSpreadOperation(statement, listVarName, "Add", out var addValue)) + { + segments.Add(SpreadSegment.FromValue(addValue.Clone())); + matchedStatements.Add(statement); + continue; + } + + if (IsAllowedPreludeNoise(statement)) + { + continue; + } + + return false; + } + + if (segments.Count == 0 || i >= boundaryIndex || string.IsNullOrEmpty(targetMemberName)) + { + return false; + } + + if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, out var collectionInitializer)) + { + return false; + } + + match = new ListPreludeMatch( + listVarName, + targetMemberName, + collectionInitializer, + matchedStatements, + i + 1 + ); + return true; + } + + private static bool TryMatchHashSetForeachSpreadPrelude( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + out ListPreludeMatch match) + { + match = default; + if (startIndex + 2 >= boundaryIndex) + { + return false; + } + + if (statements[startIndex] is not VariableDeclarationStatement setDecl + || setDecl.Variables.Count != 1 + || setDecl.Variables.FirstOrDefault() is not { } setVariable + || setVariable.Initializer is not ObjectCreateExpression setCreate + || !IsGenericCollectionType(setCreate.Type, "HashSet")) + { + return false; + } + + string setVarName = setVariable.Name; + if (string.IsNullOrEmpty(setVarName)) + { + return false; + } + + var segments = new List(); + var matchedStatements = new List { statements[startIndex] }; + string targetMemberName = string.Empty; + int i = startIndex + 1; + for (; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryMatchTerminalAssignment(statement, setVarName, out targetMemberName)) + { + matchedStatements.Add(statement); + break; + } + + if (TryMatchHashSetSpreadForeach(statement, setVarName, out var spreadEnumerable)) + { + segments.Add(SpreadSegment.FromRange(spreadEnumerable.Clone())); + matchedStatements.Add(statement); + continue; + } + + if (TryMatchCollectionSpreadOperation(statement, setVarName, "Add", out var addValue)) + { + segments.Add(SpreadSegment.FromValue(addValue.Clone())); + matchedStatements.Add(statement); + continue; + } + + if (IsAllowedPreludeNoise(statement)) + { + continue; + } + + return false; + } + + if (segments.Count == 0 || i >= boundaryIndex || string.IsNullOrEmpty(targetMemberName)) + { + return false; + } + + if (!TryBuildCollectionFromSpreadSegments(setDecl.Type, segments, out var collectionInitializer)) + { + return false; + } + + match = new ListPreludeMatch( + setVarName, + targetMemberName, + collectionInitializer, + matchedStatements, + i + 1 + ); + return true; + } + + private static bool TryMatchListSpreadBuilderWrapperAssignment( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + TypeDeclaration currentType, + out ListPreludeMatch match) + { + match = default; + if (startIndex + 2 >= boundaryIndex) + { + return false; + } + + if (statements[startIndex] is not VariableDeclarationStatement listDecl + || listDecl.Variables.Count != 1 + || listDecl.Variables.FirstOrDefault() is not { } listVariable + || listVariable.Initializer is not ObjectCreateExpression listCreate + || !IsGenericCollectionType(listCreate.Type, "List")) + { + return false; + } + + string listVarName = listVariable.Name; + if (string.IsNullOrEmpty(listVarName)) + { + return false; + } + + var segments = new List(); + var matchedStatements = new List { statements[startIndex] }; + int i = startIndex + 1; + for (; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryMatchCollectionSpreadOperation(statement, listVarName, "AddRange", out var spreadSource)) + { + segments.Add(SpreadSegment.FromRange(spreadSource.Clone())); + matchedStatements.Add(statement); + continue; + } + + if (TryMatchCollectionSpreadOperation(statement, listVarName, "Add", out var addValue)) + { + segments.Add(SpreadSegment.FromValue(addValue.Clone())); + matchedStatements.Add(statement); + continue; + } + + if (IsAllowedPreludeNoise(statement)) + { + continue; + } + + break; + } + + if (segments.Count == 0 || i >= boundaryIndex) + { + return false; + } + + if (!TryBuildCollectionFromSpreadSegments(listDecl.Type, segments, out var listBuilderInitializer)) + { + return false; + } + + if (!TryMatchSimpleAssignment(statements[i], currentType, out var targetMemberName, out var assignmentInitializer)) + { + return false; + } + + var replacementMap = new Dictionary(StringComparer.Ordinal) + { + [listVarName] = listBuilderInitializer + }; + if (!TryRewriteInitializerWithRecoveredLocals(assignmentInitializer, replacementMap, out var rewrittenInitializer)) + { + return false; + } + rewrittenInitializer = SimplifyRedundantReadOnlyListCollectionWrappers(rewrittenInitializer); + + matchedStatements.Add(statements[i]); + match = new ListPreludeMatch( + listVarName, + targetMemberName, + rewrittenInitializer, + matchedStatements, + i + 1 + ); + return true; + } + + private static bool IsGenericCollectionType(AstType type, string expectedIdentifier) + { + return type is SimpleType { Identifier: var identifier } simpleType + && string.Equals(identifier, expectedIdentifier, StringComparison.Ordinal) + && simpleType.TypeArguments.Count == 1; + } + + private static bool TryMatchCollectionSpreadOperation( + Statement statement, + string collectionVariableName, + string methodName, + out Expression argument) + { + argument = Expression.Null; + if (statement is not ExpressionStatement { Expression: InvocationExpression invocation } + || invocation.Target is not MemberReferenceExpression memberRef + || !string.Equals(memberRef.MemberName, methodName, StringComparison.Ordinal) + || memberRef.Target is not IdentifierExpression { Identifier: var targetIdentifier } + || !string.Equals(targetIdentifier, collectionVariableName, StringComparison.Ordinal)) + { + return false; + } + + if (invocation.Arguments.Count != 1) + { + return false; + } + + argument = invocation.Arguments.First().Clone(); + return true; + } + + private static bool TryMatchHashSetSpreadForeach( + Statement statement, + string setVariableName, + out Expression spreadEnumerable) + { + spreadEnumerable = Expression.Null; + if (statement is not ForeachStatement foreachStatement + || foreachStatement.EmbeddedStatement is not BlockStatement foreachBlock + || foreachBlock.Statements.Count != 1 + || foreachBlock.Statements.FirstOrDefault() is not ExpressionStatement { Expression: InvocationExpression addInvocation } + || addInvocation.Target is not MemberReferenceExpression addTarget + || !string.Equals(addTarget.MemberName, "Add", StringComparison.Ordinal) + || addTarget.Target is not IdentifierExpression { Identifier: var addTargetIdentifier } + || !string.Equals(addTargetIdentifier, setVariableName, StringComparison.Ordinal) + || addInvocation.Arguments.Count != 1 + || addInvocation.Arguments.FirstOrDefault() is not IdentifierExpression { Identifier: var addedIdentifier } + || foreachStatement.VariableDesignation is not SingleVariableDesignation { Identifier: var loopIdentifier } + || !string.Equals(addedIdentifier, loopIdentifier, StringComparison.Ordinal)) + { + return false; + } + + spreadEnumerable = foreachStatement.InExpression.Clone(); + return true; + } + + private static bool TryBuildCollectionFromSpreadSegments( + AstType collectionType, + IReadOnlyList segments, + out Expression initializer) + { + initializer = Expression.Null; + if (segments.Count == 0) + { + return false; + } + + var elements = new List(); + foreach (var segment in segments) + { + var element = segment.Expression.Clone(); + if (segment.IsSpread) + { + element.AddAnnotation(CollectionExpressionSpreadElementAnnotation.Instance); + } + elements.Add(element); + } + + var collectionExpression = new ArrayInitializerExpression(elements); + collectionExpression.AddAnnotation(CollectionExpressionArrayAnnotation.Instance); + initializer = new ObjectCreateExpression(collectionType.Clone(), collectionExpression); + return true; + } + + private static bool IsCollectionsMarshalCall(Statement statement, string methodName, string firstArgIdentifier) + { + if (statement is not ExpressionStatement { Expression: InvocationExpression invocation } + || invocation.Target is not MemberReferenceExpression memberRef + || !string.Equals(memberRef.MemberName, methodName, StringComparison.Ordinal) + || !IsCollectionsMarshalTarget(memberRef.Target)) + { + return false; + } + + if (invocation.Arguments.FirstOrDefault() is not IdentifierExpression { Identifier: var firstArg }) + { + return false; + } + + return string.Equals(firstArg, firstArgIdentifier, StringComparison.Ordinal); + } + + private static bool IsCollectionsMarshalAsSpanCall(InvocationExpression invocation, string listVarName) + { + if (invocation.Target is not MemberReferenceExpression memberRef + || !string.Equals(memberRef.MemberName, "AsSpan", StringComparison.Ordinal) + || !IsCollectionsMarshalTarget(memberRef.Target)) + { + return false; + } + + return invocation.Arguments.FirstOrDefault() is IdentifierExpression { Identifier: var firstArg } + && string.Equals(firstArg, listVarName, StringComparison.Ordinal); + } + + private static bool IsCollectionsMarshalTarget(Expression target) + { + if (target is IdentifierExpression { Identifier: "CollectionsMarshal" }) + { + return true; + } + + if (target is TypeReferenceExpression { Type: SimpleType { Identifier: "CollectionsMarshal" } }) + { + return true; + } + + return false; + } + + private static bool TryCollectSpanAssignmentValue(Statement statement, string spanVarName, out Expression value) + { + value = Expression.Null; + if (statement is not ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment }) + { + return false; + } + + if (assignment.Left is IndexerExpression + { + Target: IdentifierExpression { Identifier: var targetName } + } + && string.Equals(targetName, spanVarName, StringComparison.Ordinal)) + { + value = assignment.Right; + return true; + } + + return false; + } + + private static bool TryCollectNestedSpanAssignmentValue( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + string outerSpanVarName, + out Expression value, + out int nextIndex, + out List matchedStatements) + { + value = Expression.Null; + nextIndex = -1; + matchedStatements = []; + if (startIndex + 1 >= boundaryIndex) + { + return false; + } + + if (statements[startIndex] is not VariableDeclarationStatement refSlotDecl + || refSlotDecl.Variables.Count != 1 + || refSlotDecl.Variables.FirstOrDefault() is not { } refSlotVar + || refSlotVar.Initializer is not DirectionExpression + { + FieldDirection: FieldDirection.Ref, + Expression: IndexerExpression + { + Target: IdentifierExpression { Identifier: var slotTargetSpan } + } + } + || !string.Equals(slotTargetSpan, outerSpanVarName, StringComparison.Ordinal)) + { + return false; + } + + string slotVariableName = refSlotVar.Name; + if (string.IsNullOrEmpty(slotVariableName)) + { + return false; + } + + if (statements[startIndex + 1] is not VariableDeclarationStatement objectDecl + || objectDecl.Variables.Count != 1 + || objectDecl.Variables.FirstOrDefault() is not { } objectVar + || objectVar.Initializer is not ObjectCreateExpression objectCreate) + { + return false; + } + + string objectVariableName = objectVar.Name; + if (string.IsNullOrEmpty(objectVariableName)) + { + return false; + } + + var objectValue = (ObjectCreateExpression)objectCreate.Clone(); + objectValue.Initializer ??= new ArrayInitializerExpression(); + var assignedMembers = new HashSet(StringComparer.Ordinal); + foreach (var named in objectValue.Initializer.Elements.OfType()) + { + assignedMembers.Add(named.Name); + } + + matchedStatements.Add(statements[startIndex]); + matchedStatements.Add(statements[startIndex + 1]); + + for (int i = startIndex + 2; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryMatchRefSlotAssignment(statement, slotVariableName, objectVariableName)) + { + matchedStatements.Add(statement); + value = objectValue; + nextIndex = i + 1; + return true; + } + + if (TryMatchObjectMemberListPrelude( + statements, + i, + boundaryIndex, + objectVariableName, + out var listMemberName, + out var listMemberValue, + out var listMatchedStatements, + out var listNextIndex)) + { + if (!assignedMembers.Add(listMemberName)) + { + return false; + } + + AddObjectMemberInitializer(objectValue, listMemberName, listMemberValue); + matchedStatements.AddRange(listMatchedStatements); + i = listNextIndex - 1; + continue; + } + + if (TryMatchObjectMemberSimpleAssignment(statement, objectVariableName, out var memberName, out var memberValue)) + { + if (!assignedMembers.Add(memberName)) + { + return false; + } + + AddObjectMemberInitializer(objectValue, memberName, memberValue); + matchedStatements.Add(statement); + continue; + } + + if (IsAllowedPreludeNoise(statement)) + { + matchedStatements.Add(statement); + continue; + } + + return false; + } + + return false; + } + + private static bool TryMatchObjectMemberListPrelude( + IReadOnlyList statements, + int startIndex, + int boundaryIndex, + string objectVariableName, + out string memberName, + out Expression initializerValue, + out List matchedStatements, + out int nextIndex) + { + memberName = string.Empty; + initializerValue = Expression.Null; + matchedStatements = []; + nextIndex = -1; + if (startIndex + 3 >= boundaryIndex) + { + return false; + } + + if (statements[startIndex] is not VariableDeclarationStatement listDecl + || listDecl.Variables.Count != 1 + || listDecl.Variables.FirstOrDefault() is not { } listVariable + || listVariable.Initializer is not ObjectCreateExpression) + { + return false; + } + + string listVarName = listVariable.Name; + if (string.IsNullOrEmpty(listVarName)) + { + return false; + } + + if (!IsCollectionsMarshalCall(statements[startIndex + 1], "SetCount", listVarName)) + { + return false; + } + + if (statements[startIndex + 2] is not VariableDeclarationStatement spanDecl + || spanDecl.Variables.Count != 1 + || spanDecl.Variables.FirstOrDefault() is not { } spanVariable + || spanVariable.Initializer is not InvocationExpression spanInit + || !IsCollectionsMarshalAsSpanCall(spanInit, listVarName)) + { + return false; + } + + string spanVarName = spanVariable.Name; + var values = new List(); + matchedStatements.Add(statements[startIndex]); + matchedStatements.Add(statements[startIndex + 1]); + matchedStatements.Add(statements[startIndex + 2]); + + for (int i = startIndex + 3; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryMatchTerminalObjectMemberAssignment(statement, listVarName, objectVariableName, out memberName)) + { + matchedStatements.Add(statement); + initializerValue = new ObjectCreateExpression(listDecl.Type.Clone()) + { + Initializer = new ArrayInitializerExpression(values) + }; + nextIndex = i + 1; + return true; + } + + if (TryCollectSpanAssignmentValue(statement, spanVarName, out var value)) + { + values.Add(value.Clone()); + matchedStatements.Add(statement); + continue; + } + + if (IsAllowedPreludeNoise(statement)) + { + matchedStatements.Add(statement); + continue; + } + + return false; + } + + return false; + } + + private static bool TryMatchObjectMemberSimpleAssignment( + Statement statement, + string objectVariableName, + out string memberName, + out Expression memberValue) + { + memberName = string.Empty; + memberValue = Expression.Null; + if (statement is not ExpressionStatement + { + Expression: AssignmentExpression + { + Operator: AssignmentOperatorType.Assign, + Left: MemberReferenceExpression + { + Target: IdentifierExpression { Identifier: var targetName }, + MemberName: var assignedMemberName + }, + Right: var right + } + } + || !string.Equals(targetName, objectVariableName, StringComparison.Ordinal)) + { + return false; + } + + if (!IsAllowedSimpleInitializerExpression(right)) + { + return false; + } + + memberName = assignedMemberName; + memberValue = right; + return true; + } + + private static bool TryMatchTerminalObjectMemberAssignment( + Statement statement, + string listVarName, + string objectVariableName, + out string memberName) + { + memberName = string.Empty; + if (statement is not ExpressionStatement + { + Expression: AssignmentExpression + { + Operator: AssignmentOperatorType.Assign, + Left: MemberReferenceExpression + { + Target: IdentifierExpression { Identifier: var targetName }, + MemberName: var assignedMemberName + }, + Right: IdentifierExpression { Identifier: var rightName } + } + } + || !string.Equals(targetName, objectVariableName, StringComparison.Ordinal) + || !string.Equals(rightName, listVarName, StringComparison.Ordinal)) + { + return false; + } + + memberName = assignedMemberName; + return true; + } + + private static bool TryMatchRefSlotAssignment(Statement statement, string slotVariableName, string objectVariableName) + { + if (statement is not ExpressionStatement + { + Expression: AssignmentExpression + { + Operator: AssignmentOperatorType.Assign, + Left: IdentifierExpression { Identifier: var leftName }, + Right: IdentifierExpression { Identifier: var rightName } + } + }) + { + return false; + } + + return string.Equals(leftName, slotVariableName, StringComparison.Ordinal) + && string.Equals(rightName, objectVariableName, StringComparison.Ordinal); + } + + private static void AddObjectMemberInitializer(ObjectCreateExpression objectValue, string memberName, Expression memberValue) + { + objectValue.Initializer ??= new ArrayInitializerExpression(); + objectValue.Initializer.Elements.Add(new NamedExpression(memberName, memberValue.Clone())); + } + + private static bool IsAllowedPreludeNoise(Statement statement) + { + // Numeric temp declarations and increments are common in this lowering pattern. + if (statement is VariableDeclarationStatement + { + Variables.Count: 1 + } variableDecl + && variableDecl.Variables.FirstOrDefault() is { Name: var varName } + && IsTempCounterName(varName)) + { + return true; + } + + if (statement is ExpressionStatement + { + Expression: UnaryOperatorExpression + { + Operator: UnaryOperatorType.PostIncrement, + Expression: IdentifierExpression { Identifier: var incrementIdentifier } + } + } + && IsTempCounterName(incrementIdentifier)) + { + return true; + } + + if (statement is ExpressionStatement + { + Expression: AssignmentExpression + { + Operator: AssignmentOperatorType.Assign, + Left: IdentifierExpression { Identifier: var assignmentIdentifier } + } + } + && IsTempCounterName(assignmentIdentifier)) + { + return true; + } + + return false; + } + + private static bool IsTempCounterName(string identifier) + { + return identifier.StartsWith("num", StringComparison.Ordinal); + } + + private static void CleanupLeadingTempNoise(ConstructorDeclaration constructorDeclaration) + { + var statements = constructorDeclaration.Body.Statements.ToArray(); + var prefixNoise = new List(); + + foreach (var statement in statements) + { + if (!IsAllowedPreludeNoise(statement)) + { + break; + } + + prefixNoise.Add(statement); + } + + if (prefixNoise.Count == 0) + { + return; + } + + var statementsAfterNoise = statements.Skip(prefixNoise.Count).ToArray(); + foreach (var statement in prefixNoise) + { + // Declarations are removed only when no longer referenced after the generated prelude. + if (statement is VariableDeclarationStatement + { + Variables.Count: 1 + } declaration + && declaration.Variables.FirstOrDefault() is { Name: var name } + && IsTempCounterName(name)) + { + if (!IsIdentifierReferencedOutsidePrefix(name, statementsAfterNoise, constructorDeclaration.Initializer)) + { + statement.Remove(); + } + + continue; + } + + statement.Remove(); + } + } + + private static bool IsIdentifierReferencedOutsidePrefix( + string identifier, + IReadOnlyList statementsAfterPrefix, + ConstructorInitializer constructorInitializer) + { + foreach (var statement in statementsAfterPrefix) + { + if (statement.Descendants.OfType() + .Any(id => string.Equals(id.Identifier, identifier, StringComparison.Ordinal))) + { + return true; + } + } + + if (!constructorInitializer.IsNull + && constructorInitializer.Descendants.OfType() + .Any(id => string.Equals(id.Identifier, identifier, StringComparison.Ordinal))) + { + return true; + } + + return false; + } + + private static bool TryMatchTerminalAssignment(Statement statement, string listVarName, out string targetMemberName) + { + targetMemberName = string.Empty; + if (statement is not ExpressionStatement { Expression: AssignmentExpression { Operator: AssignmentOperatorType.Assign } assignment }) + { + return false; + } + + if (!TryGetAssignedMemberName(assignment.Left, out targetMemberName)) + { + return false; + } + + return assignment.Right is IdentifierExpression { Identifier: var rightName } + && string.Equals(rightName, listVarName, StringComparison.Ordinal); + } + + private static bool TryBuildConstructorInitializer( + ConstructorDeclaration constructorDeclaration, + IReadOnlyList statements, + int boundaryIndex, + Statement boundaryStatement, + Dictionary matchedListInitByLocal, + out ConstructorInitializer constructorInitializer, + out List matchedCtorPreludeStatements) + { + constructorInitializer = ConstructorInitializer.Null; + matchedCtorPreludeStatements = []; + if (!TryGetCtorInvocation(boundaryStatement, out var initType, out var invocation)) + { + return false; + } + + var newInitializer = new ConstructorInitializer + { + ConstructorInitializerType = initType + }; + + foreach (var arg in invocation.Arguments) + { + switch (arg) + { + case IdentifierExpression { Identifier: var localName } when matchedListInitByLocal.TryGetValue(localName, out var recoveredExpression): + newInitializer.Arguments.Add(recoveredExpression.Clone()); + break; + case IdentifierExpression { Identifier: var localName }: + if (constructorDeclaration.Parameters.Any(parameter => parameter.Name == localName)) + { + newInitializer.Arguments.Add(arg.Clone()); + break; + } + if (!TryMatchCtorArgumentListPrelude( + statements, + boundaryIndex, + localName, + out var ctorArgInitializer, + out var matchedStatements)) + { + return false; + } + newInitializer.Arguments.Add(ctorArgInitializer); + matchedCtorPreludeStatements.AddRange(matchedStatements); + break; + case PrimitiveExpression: + case NullReferenceExpression: + case ObjectCreateExpression: + newInitializer.Arguments.Add(arg.Clone()); + break; + default: + return false; + } + } + + constructorInitializer = newInitializer; + return true; + } + + private static bool TryMatchCtorArgumentListPrelude( + IReadOnlyList statements, + int boundaryIndex, + string listVarName, + out Expression initializer, + out List matchedStatements) + { + initializer = Expression.Null; + matchedStatements = []; + int startIndex = -1; + VariableDeclarationStatement? listDecl = null; + for (int i = 0; i < boundaryIndex; i++) + { + if (statements[i] is VariableDeclarationStatement variableDeclaration + && variableDeclaration.Variables.Count == 1 + && variableDeclaration.Variables.FirstOrDefault() is { } variable + && string.Equals(variable.Name, listVarName, StringComparison.Ordinal) + && variable.Initializer is ObjectCreateExpression) + { + startIndex = i; + listDecl = variableDeclaration; + } + } + + if (startIndex < 0 || listDecl == null || startIndex + 2 >= boundaryIndex) + { + return false; + } + + if (!IsCollectionsMarshalCall(statements[startIndex + 1], "SetCount", listVarName)) + { + return false; + } + + if (statements[startIndex + 2] is not VariableDeclarationStatement spanDecl + || spanDecl.Variables.Count != 1 + || spanDecl.Variables.FirstOrDefault() is not { } spanVariable + || spanVariable.Initializer is not InvocationExpression spanInit + || !IsCollectionsMarshalAsSpanCall(spanInit, listVarName)) + { + return false; + } + + string spanVarName = spanVariable.Name; + var values = new List(); + for (int i = startIndex + 3; i < boundaryIndex; i++) + { + var statement = statements[i]; + if (TryCollectSpanAssignmentValue(statement, spanVarName, out var value)) + { + values.Add(value.Clone()); + continue; + } + + if (!IsAllowedPreludeNoise(statement)) + { + return false; + } + } + + if (values.Count == 0) + { + return false; + } + + initializer = new ObjectCreateExpression(listDecl.Type.Clone()) + { + Initializer = new ArrayInitializerExpression(values) + }; + for (int i = startIndex; i < boundaryIndex; i++) + { + matchedStatements.Add(statements[i]); + } + return true; + } + + private static void ReorderRecoveredMembers( + TypeDeclaration typeDeclaration, + List recoveredMemberOrder, + Dictionary memberMap) + { + var uniqueNames = recoveredMemberOrder.Distinct(StringComparer.Ordinal).ToList(); + if (uniqueNames.Count < 2) + { + return; + } + + var participatingMembers = uniqueNames + .Where(memberMap.ContainsKey) + .Select(name => memberMap[name]) + .Distinct() + .ToList(); + if (participatingMembers.Count < 2) + { + return; + } + + var currentMembers = typeDeclaration.Members.ToArray(); + var participatingIndices = currentMembers + .Select((member, index) => (member, index)) + .Where(tuple => participatingMembers.Contains(tuple.member)) + .Select(tuple => tuple.index) + .ToArray(); + if (participatingIndices.Length < 2) + { + return; + } + var participatingSet = participatingMembers.ToHashSet(); + var reorderedParticipants = uniqueNames + .Select(name => memberMap.TryGetValue(name, out var member) ? member : null) + .Where(member => member != null && participatingSet.Contains(member)) + .Distinct() + .Cast() + .ToList(); + if (reorderedParticipants.Count != participatingMembers.Count) + { + // Ambiguous mapping; keep declaration order unchanged. + return; + } + + var rewrittenMembers = currentMembers.ToArray(); + int nextParticipant = 0; + for (int i = 0; i < currentMembers.Length; i++) + { + if (participatingSet.Contains(currentMembers[i])) + { + rewrittenMembers[i] = reorderedParticipants[nextParticipant++]; + } + } + + foreach (var member in currentMembers) + { + member.Remove(); + } + + foreach (var member in rewrittenMembers) + { + typeDeclaration.InsertChildBefore(typeDeclaration.RBraceToken, member, Roles.TypeMemberRole); + } + } + + private readonly record struct ListPreludeMatch( + string ListVariableName, + string TargetMemberName, + Expression InitializerExpression, + List MatchedStatements, + int NextIndex + ); + + private readonly record struct ConditionalTempAssignmentMatch( + string TargetMemberName, + Expression InitializerExpression, + List MatchedStatements, + int NextIndex + ); + + private readonly record struct SpreadSegment(bool IsSpread, Expression Expression) + { + public static SpreadSegment FromRange(Expression expression) + { + return new SpreadSegment(true, expression); + } + + public static SpreadSegment FromValue(Expression expression) + { + return new SpreadSegment(false, expression); + } + } +} diff --git a/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs b/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs index 597c1a11..6f5e3fb7 100644 --- a/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs +++ b/godot-mono-decomp/GodotMonoDecomp/NuGetUtils.cs @@ -26,7 +26,7 @@ public static async Task DownloadFileTaskAsync(this HttpClient client, Uri uri, public static class NugetDetails { - public static async Task ResolvePackageAndGetContentHash(string name, string version, CancellationToken cancellationToken) + public static async Task ResolvePackageAndGetContentHash(string name, string version, bool checkOnline, CancellationToken cancellationToken) { // download the package to the local cache string? p = null; @@ -59,10 +59,13 @@ public static class NugetDetails // either we didn't have a metadata file or it wasn't from nuget.org, so we need to download it again // save it to a temporary directory so as not to clobber the local cache var tempDir = Path.Combine(NuGetEnvironment.GetFolderPath(NuGetFolderPath.Temp), name.ToLower(), version.ToLower()); - p = await DownloadPackageFromNugetAsync(name, version, tempDir, cancellationToken); + if (checkOnline) + { + p = await DownloadPackageFromNugetAsync(name, version, tempDir, cancellationToken); + } } - else + else if (checkOnline) { p = await DownloadPackageToLocalCache(name, version, cancellationToken); } diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs b/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs new file mode 100644 index 00000000..c516cae3 --- /dev/null +++ b/godot-mono-decomp/GodotMonoDecomp/RemoveBogusBaseConstructorCalls.cs @@ -0,0 +1,50 @@ +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; + +namespace GodotMonoDecomp; + +/// +/// Removes erroneous base._002Ector() calls that sometimes appear at the end of +/// constructor bodies in decompiled output. +/// +public class RemoveBogusBaseConstructorCalls : DepthFirstAstVisitor, IAstTransform +{ + ConstructorDeclaration? currentConstructor; + + public void Run(AstNode rootNode, TransformContext context) + { + rootNode.AcceptVisitor(this); + } + + public override void VisitConstructorDeclaration(ConstructorDeclaration constructorDeclaration) + { + var previousConstructor = currentConstructor; + currentConstructor = constructorDeclaration; + try + { + base.VisitConstructorDeclaration(constructorDeclaration); + } + finally + { + currentConstructor = previousConstructor; + } + } + + public override void VisitInvocationExpression(InvocationExpression invocationExpression) + { + if (currentConstructor?.Body != null + && invocationExpression.Arguments.Count == 0 + && invocationExpression.Target is MemberReferenceExpression memberReference + && memberReference.MemberName == "_002Ector" + && memberReference.Target is BaseReferenceExpression + && invocationExpression.Parent is ExpressionStatement expressionStatement + && expressionStatement.Parent == currentConstructor.Body + && currentConstructor.Body.Statements.LastOrDefault() == expressionStatement) + { + expressionStatement.Remove(); + return; + } + + base.VisitInvocationExpression(invocationExpression); + } +} diff --git a/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs b/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs new file mode 100644 index 00000000..0cba4cf8 --- /dev/null +++ b/godot-mono-decomp/GodotMonoDecomp/RemoveJsonSourceGenerationClassBody.cs @@ -0,0 +1,176 @@ +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; +using ICSharpCode.Decompiler.Semantics; +using ICSharpCode.Decompiler.TypeSystem; + +namespace GodotMonoDecomp; + +/// +/// Restores source-level shape for System.Text.Json source-generation context classes. +/// +public class RemoveJsonSourceGenerationClassBody : DepthFirstAstVisitor, IAstTransform +{ + private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute"; + private const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute"; + private const string JsonSourceGeneratorName = "System.Text.Json.SourceGeneration"; + private const string JsonTypeInfoFullName = "System.Text.Json.Serialization.Metadata.JsonTypeInfo"; + + public void Run(AstNode rootNode, TransformContext context) + { + rootNode.AcceptVisitor(this); + } + + public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration) + { + if (typeDeclaration.ClassType != ClassType.Class) + { + base.VisitTypeDeclaration(typeDeclaration); + return; + } + + bool hasJsonSourceGeneratorGeneratedCode = Common.RemoveGeneratedCodeAttributes( + typeDeclaration.Attributes, + JsonSourceGeneratorName + ); + if (!hasJsonSourceGeneratorGeneratedCode) + { + base.VisitTypeDeclaration(typeDeclaration); + return; + } + // get all the parameters for the `JsonSerializable` attribute + List jsonTypes = typeDeclaration.Attributes + .SelectMany(attribute => attribute.Attributes) + .Where(attribute => Common.IsAttribute(attribute, JsonSerializableAttributeFullName, "JsonSerializable")) + .SelectMany(attribute => attribute.Arguments) + .Select(argument => + { + if (argument is TypeOfExpression typeOfExpression) + { + return typeOfExpression.Type.Annotation()?.Type; + } + return null; + }) + .OfType() + .Distinct() + .ToList(); + + typeDeclaration.Modifiers |= Modifiers.Partial; + var members = typeDeclaration.Members.ToArray(); + int firstJsonTypeInfoFieldIndex = GetFirstIndex(members, member => + member is FieldDeclaration field && IsJsonTypeInfoTypeWithTemplateArgumentsOf(field.ReturnType, jsonTypes) + ); + int firstJsonTypeInfoPropertyIndex = GetFirstIndex(members, member => + member is PropertyDeclaration property && IsJsonTypeInfoTypeWithTemplateArgumentsOf(property.ReturnType, jsonTypes) + ); + int firstJsonTypeInfoMethodIndex = GetFirstIndex(members, member => + member is MethodDeclaration method && IsJsonTypeInfoTypeWithTemplateArgumentsOf(method.ReturnType, jsonTypes) + ); + + for (int i = 0; i < members.Length; i++) + { + var member = members[i]; + if (ShouldPreserveMember( + member, + i, + firstJsonTypeInfoFieldIndex, + firstJsonTypeInfoPropertyIndex, + firstJsonTypeInfoMethodIndex)) + { + continue; + } + + member.Remove(); + } + } + + private static bool ShouldPreserveMember( + EntityDeclaration member, + int memberIndex, + int firstJsonTypeInfoFieldIndex, + int firstJsonTypeInfoPropertyIndex, + int firstJsonTypeInfoMethodIndex) + { + // Preserve any leading user member before the generated JsonTypeInfo field section. + if (firstJsonTypeInfoFieldIndex >= 0 && memberIndex < firstJsonTypeInfoFieldIndex) + { + return true; + } + + // Preserve leading user properties before generated JsonTypeInfo properties. + if (member is PropertyDeclaration + && firstJsonTypeInfoPropertyIndex >= 0 + && memberIndex < firstJsonTypeInfoPropertyIndex) + { + return true; + } + + // Preserve leading user methods before generated JsonTypeInfo-returning methods. + if (member is MethodDeclaration + && firstJsonTypeInfoMethodIndex >= 0 + && memberIndex < firstJsonTypeInfoMethodIndex) + { + return true; + } + + return false; + } + + private static int GetFirstIndex(EntityDeclaration[] members, Func predicate) + { + for (int i = 0; i < members.Length; i++) + { + if (predicate(members[i])) + { + return i; + } + } + + return -1; + } + + private static bool IsJsonTypeInfoType(AstType type) + { + if (type.IsNull) + { + return false; + } + + if (type.Annotation() is { Type: { } resolvedType }) + { + if (string.Equals(resolvedType.FullName, JsonTypeInfoFullName, StringComparison.Ordinal)) + { + return true; + } + + if (resolvedType.GetDefinition() is ITypeDefinition definition + && string.Equals(definition.FullName, JsonTypeInfoFullName, StringComparison.Ordinal)) + { + return true; + } + } + + // Fallback for cases where type resolution annotations are unavailable. + return type.ToString().Contains("JsonTypeInfo", StringComparison.Ordinal); + } + + private static bool IsJsonTypeInfoTypeWithTemplateArgumentsOf(AstType type, IEnumerable jsonTypes) + { + if (IsJsonTypeInfoType(type)) + { + if (type.Annotation() is { Type: { } resolvedType }) + { + if (resolvedType.TypeArguments.All(parameter => jsonTypes.Any(jsonType => + { + return jsonType.Equals(parameter.GetDefinition()); + }))) + { + return true; + } + } + } + return false; + } + + +} diff --git a/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs b/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs new file mode 100644 index 00000000..fe411abc --- /dev/null +++ b/godot-mono-decomp/GodotMonoDecomp/RestoreGeneratedRegexMethods.cs @@ -0,0 +1,94 @@ +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; +using ICSharpCode.Decompiler.Semantics; +using ICSharpCode.Decompiler.TypeSystem; + +namespace GodotMonoDecomp; + +/// +/// Restores source-level shape for methods generated by the Regex source generator. +/// +public class RestoreGeneratedRegexMethods : DepthFirstAstVisitor, IAstTransform +{ + private const string GeneratedRegexAttributeFullName = "System.Text.RegularExpressions.GeneratedRegexAttribute"; + private const string GeneratedCodeAttributeFullName = "System.CodeDom.Compiler.GeneratedCodeAttribute"; + private const string RegexGeneratorName = "System.Text.RegularExpressions.Generator"; + + public void Run(AstNode rootNode, TransformContext context) + { + rootNode.AcceptVisitor(this); + } + + public override void VisitMethodDeclaration(MethodDeclaration methodDeclaration) + { + if (!HasGeneratedRegexAttribute(methodDeclaration)) + { + base.VisitMethodDeclaration(methodDeclaration); + return; + } + + bool hasRegexGeneratorGeneratedCode = Common.RemoveGeneratedCodeAttributes(methodDeclaration.Attributes, RegexGeneratorName); + bool hasGeneratedBodyPattern = HasGeneratedRegexBodyPattern(methodDeclaration); + if (!hasGeneratedBodyPattern && !hasRegexGeneratorGeneratedCode) + { + base.VisitMethodDeclaration(methodDeclaration); + return; + } + + methodDeclaration.Modifiers |= Modifiers.Partial; + methodDeclaration.Body = null; + if (methodDeclaration.Parent is TypeDeclaration containingType) + { + containingType.Modifiers |= Modifiers.Partial; + } + } + + private static bool HasGeneratedRegexAttribute(MethodDeclaration methodDeclaration) + { + foreach (var section in methodDeclaration.Attributes) + { + foreach (var attribute in section.Attributes) + { + if (Common.IsAttribute(attribute, GeneratedRegexAttributeFullName, "GeneratedRegex")) + { + return true; + } + } + } + + return false; + } + + private static bool RemoveRegexGeneratorGeneratedCodeAttributes(MethodDeclaration methodDeclaration) + { + return Common.RemoveGeneratedCodeAttributes(methodDeclaration.Attributes, RegexGeneratorName); + } + + private static bool HasGeneratedRegexBodyPattern(MethodDeclaration methodDeclaration) + { + if (methodDeclaration.Body is not { Statements.Count: 1 }) + { + return false; + } + + if (methodDeclaration.Body.Statements.First() is not ReturnStatement returnStatement) + { + return false; + } + + if (returnStatement.Expression is not MemberReferenceExpression memberReference) + { + return false; + } + + if (!string.Equals(memberReference.MemberName, "Instance", StringComparison.Ordinal)) + { + return false; + } + + string targetText = memberReference.Target.ToString(); + return targetText.Contains("RegexGenerator", StringComparison.Ordinal) + || targetText.Contains("_003CRegexGenerator", StringComparison.Ordinal); + } +} diff --git a/godot-mono-decomp/GodotMonoDecompCLI/Program.cs b/godot-mono-decomp/GodotMonoDecompCLI/Program.cs index c2fd736b..a9e17715 100644 --- a/godot-mono-decomp/GodotMonoDecompCLI/Program.cs +++ b/godot-mono-decomp/GodotMonoDecompCLI/Program.cs @@ -1,4 +1,4 @@ -// See https://aka.ms/new-console-template for more information +// See https://aka.ms/new-console-template for more information using System.Text; using GodotMonoDecomp; @@ -43,6 +43,7 @@ int Main(string[] args) settings.CreateAdditionalProjectsForProjectReferences = !result.Value.NoCreateAdditionalProjectsForProjectReferences; settings.VerifyNuGetPackageIsFromNugetOrg = result.Value.VerifyNuGetPackages; settings.GodotVersionOverride = result.Value.GodotVersion == null ? null : GodotStuff.ParseGodotVersionFromString(result.Value.GodotVersion); + settings.EnableCollectionInitializerLifting = !result.Value.DisableCollectionInitializerLifting || result.Value.EnableCollectionInitializerLifting; // get the current time var startTime = DateTime.Now; // call the DecompileProject function @@ -106,6 +107,12 @@ public class Options [Option("no-multi-project", Required = false, HelpText = "Whether to create additional projects for project references in main module.")] public bool NoCreateAdditionalProjectsForProjectReferences { get; set; } + [Option("enable-collection-initializer-lifting", Required = false, HelpText = "Enable LiftCollectionInitializers and disable RemoveBogusBaseConstructorCalls.")] + public bool EnableCollectionInitializerLifting { get; set; } + + [Option("disable-collection-initializer-lifting", Required = false, HelpText = "Disable LiftCollectionInitializers and run RemoveBogusBaseConstructorCalls instead.")] + public bool DisableCollectionInitializerLifting { get; set; } + [Option("write-script-info", Required = false, HelpText = "Write script info to a JSON file in the output directory.")] public bool WriteScriptInfo { get; set; } // dump strings option diff --git a/godot-mono-decomp/collection_examples/decompiled/TestCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/decompiled/TestCollectionExpressionInitializers.cs new file mode 100644 index 00000000..8b6df54c --- /dev/null +++ b/godot-mono-decomp/collection_examples/decompiled/TestCollectionExpressionInitializers.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ConsoleApp2; + +public static class TestCollectionExpressionInitializers +{ + public class TestClassBase + { + public readonly int parentIntField = 1; + + public readonly List numbers; + + public readonly string secondField; + + public TestClassBase(List numbers) + { + this.numbers = numbers; + secondField = "second string"; + } + } + + public class ElemClass + { + public int elem; + } + + public class TestClass1 : TestClassBase + { + public readonly int bar = 2; + + public readonly List strings; + + public static readonly List elems; + + public readonly Dictionary dict; + + public int randomField; + + public string anotherRandomField; + + public List fieldInitializedInConstructor; + + // 'set' not in original order; was originally after `IntProp2` + public readonly HashSet set; + + public int IntProp1 { get; set; } + + public List ListProp1 { get; set; } + + public int IntProp2 { get; set; } + + public List StringListProp1 { get; set; } + + public TestClass1() + { + // `strings` initialization + int num = 5; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = "one"; + num2++; + span[num2] = "two"; + num2++; + span[num2] = "three"; + num2++; + span[num2] = "four"; + span[num2 + 1] = "five"; + strings = list; + // `dict` initialization + dict = new Dictionary + { + ["one"] = 1, + ["two"] = 2, + ["three"] = 3, + ["four"] = 4, + ["five"] = 5 + }; + // `IntProp1` initialization + IntProp1 = 1; + // `ListProp1` initialization + num2 = 5; + List list2 = new List(num2); + CollectionsMarshal.SetCount(list2, num2); + Span span2 = CollectionsMarshal.AsSpan(list2); + num = 0; + span2[num] = 4; + num++; + span2[num] = 5; + num++; + span2[num] = 6; + num++; + span2[num] = 7; + span2[num + 1] = 8; + ListProp1 = list2; + // `IntProp2` initialization + IntProp2 = 2; + // `set` initialization + // Even though `set` is not listed in the class members in the original order, `set` initialization still happens in original order (i.e. after `IntProp2`) + set = new HashSet { 1, 2, 3, 4, 5 }; + // `StringListProp1` initialization + num = 5; + List list3 = new List(num); + CollectionsMarshal.SetCount(list3, num); + Span span3 = CollectionsMarshal.AsSpan(list3); + num2 = 0; + span3[num2] = "nine"; + num2++; + span3[num2] = "ten"; + num2++; + span3[num2] = "eleven"; + num2++; + span3[num2] = "twelve"; + span3[num2 + 1] = "thirteen"; + StringListProp1 = list3; + // initialization of the List parameter passed to the base TestClassBase constructor, which uses it to initialize `TestClassBase.numbers` + num2 = 5; + List list4 = new List(num2); + CollectionsMarshal.SetCount(list4, num2); + Span span4 = CollectionsMarshal.AsSpan(list4); + num = 0; + span4[num] = 1; + num++; + span4[num] = 2; + num++; + span4[num] = 3; + num++; + span4[num] = 4; + span4[num + 1] = 5; + // call to base constructor, indicates that we've reached the end of the originally inlined field/property initializers + base._002Ector(list4); + + // Stuff that was in the original constructor body + foo(); + num = 5; + List list5 = new List(num); + CollectionsMarshal.SetCount(list5, num); + Span span5 = CollectionsMarshal.AsSpan(list5); + num2 = 0; + span5[num2] = "string_1"; + num2++; + span5[num2] = "string_2"; + num2++; + span5[num2] = "string_3"; + num2++; + span5[num2] = "string_4"; + num2++; + span5[num2] = "string_5"; + fieldInitializedInConstructor = list5; + anotherRandomField = "another string"; + } + + private void foo() + { + randomField = 1; + } + + static TestClass1() + { + int num = 5; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = new ElemClass + { + elem = 1 + }; + num2++; + span[num2] = new ElemClass + { + elem = 2 + }; + num2++; + span[num2] = new ElemClass + { + elem = 3 + }; + num2++; + span[num2] = new ElemClass + { + elem = 4 + }; + num2++; + span[num2] = new ElemClass + { + elem = 5 + }; + elems = list; + } + } +} diff --git a/godot-mono-decomp/collection_examples/decompiled/TestCollectionInitWithSpread.cs b/godot-mono-decomp/collection_examples/decompiled/TestCollectionInitWithSpread.cs new file mode 100644 index 00000000..10bc0152 --- /dev/null +++ b/godot-mono-decomp/collection_examples/decompiled/TestCollectionInitWithSpread.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestCollectionInitWithSpread +{ + public class TestStaticHashSetMemberSpreadInit + { + public static readonly HashSet strings; + + static TestStaticHashSetMemberSpreadInit() + { + HashSet hashSet = new HashSet(); + foreach (string item in stringListConst1) + { + hashSet.Add(item); + } + foreach (string item2 in stringListConst2) + { + hashSet.Add(item2); + } + foreach (string item3 in stringListConst3) + { + hashSet.Add(item3); + } + hashSet.Add("sixteen"); + hashSet.Add("seventeen"); + hashSet.Add("eighteen"); + hashSet.Add("nineteen"); + hashSet.Add("twenty"); + strings = hashSet; + } + } + + public class TestStaticListMemberSpreadInit + { + public static readonly List strings; + + static TestStaticListMemberSpreadInit() + { + List list = new List(); + list.AddRange(stringListConst1); + list.AddRange(stringListConst2); + list.AddRange(stringListConst3); + list.Add("sixteen"); + list.Add("seventeen"); + list.Add("eighteen"); + list.Add("nineteen"); + list.Add("twenty"); + strings = list; + } + } + + public class TestStaticReadOnlySetMemberSpreadInit + { + public static readonly IReadOnlySet strings; + + static TestStaticReadOnlySetMemberSpreadInit() + { + List list = new List(); + list.AddRange(stringListConst1); + list.AddRange(stringListConst2); + list.AddRange(stringListConst3); + list.Add("sixteen"); + list.Add("seventeen"); + list.Add("eighteen"); + list.Add("nineteen"); + list.Add("twenty"); + strings = new HashSet(new _003C_003Ez__ReadOnlyList(list)); + } + } + + public class TestStaticReadOnlyListMemberSpreadInit + { + public static readonly IReadOnlyList strings; + + static TestStaticReadOnlyListMemberSpreadInit() + { + List list = new List(); + list.AddRange(stringListConst1); + list.AddRange(stringListConst2); + list.AddRange(stringListConst3); + list.Add("sixteen"); + list.Add("seventeen"); + list.Add("eighteen"); + list.Add("nineteen"); + list.Add("twenty"); + strings = new _003C_003Ez__ReadOnlyList(list); + } + } + + private static IEnumerable stringListConst1 => new _003C_003Ez__ReadOnlyArray(new string[5] { "one", "two", "three", "four", "five" }); + + private static IEnumerable stringListConst2 => new _003C_003Ez__ReadOnlyArray(new string[5] { "six", "seven", "eight", "nine", "ten" }); + + private static IEnumerable stringListConst3 => new _003C_003Ez__ReadOnlyArray(new string[5] { "eleven", "twelve", "thirteen", "fourteen", "fifteen" }); +} diff --git a/godot-mono-decomp/collection_examples/decompiled/TestCtorBoundaryCoverage.cs b/godot-mono-decomp/collection_examples/decompiled/TestCtorBoundaryCoverage.cs new file mode 100644 index 00000000..d47fc8ba --- /dev/null +++ b/godot-mono-decomp/collection_examples/decompiled/TestCtorBoundaryCoverage.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ConsoleApp2; + +public static class TestCtorBoundaryCoverage +{ + public class BoundaryBase + { + public readonly List baseNumbers; + + public BoundaryBase(List baseNumbers) + { + this.baseNumbers = baseNumbers; + } + } + + public class TailBoundaryDerived : BoundaryBase + { + public readonly List names; + + public TailBoundaryDerived() + { + int num = 3; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = "alpha"; + num2++; + span[num2] = "beta"; + span[num2 + 1] = "gamma"; + names = list; + num2 = 3; + List list2 = new List(num2); + CollectionsMarshal.SetCount(list2, num2); + Span span2 = CollectionsMarshal.AsSpan(list2); + num = 0; + span2[num] = 1; + num++; + span2[num] = 2; + span2[num + 1] = 3; + base._002Ector(list2); + } + } + + public class TransitionBoundaryDerived : BoundaryBase + { + public readonly List values; + + public string data; + + public TransitionBoundaryDerived() + { + int num = 3; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = 10; + num2++; + span[num2] = 20; + span[num2 + 1] = 30; + values = list; + num2 = 3; + List list2 = new List(num2); + CollectionsMarshal.SetCount(list2, num2); + Span span2 = CollectionsMarshal.AsSpan(list2); + num = 0; + span2[num] = 4; + num++; + span2[num] = 5; + span2[num + 1] = 6; + base._002Ector(list2); + data = ComputeData(); + } + + private static string ComputeData() + { + return "from-function"; + } + } +} diff --git a/godot-mono-decomp/collection_examples/decompiled/TestFuncInitializer.cs b/godot-mono-decomp/collection_examples/decompiled/TestFuncInitializer.cs new file mode 100644 index 00000000..bde3fdb3 --- /dev/null +++ b/godot-mono-decomp/collection_examples/decompiled/TestFuncInitializer.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ConsoleApp2; + +public static class TestFuncInitializer +{ + public class TestClass1 + { + public readonly List strings; + + private Func filter; + + public TestClass1() + { + int num = 5; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = "one"; + num2++; + span[num2] = "two"; + num2++; + span[num2] = "three"; + num2++; + span[num2] = "four"; + span[num2 + 1] = "five"; + strings = list; + filter = (string s) => s.Length > 3; + base._002Ector(); + } + } +} diff --git a/godot-mono-decomp/collection_examples/decompiled/TestInterleavedStaticCollectionInit.cs b/godot-mono-decomp/collection_examples/decompiled/TestInterleavedStaticCollectionInit.cs new file mode 100644 index 00000000..1b569820 --- /dev/null +++ b/godot-mono-decomp/collection_examples/decompiled/TestInterleavedStaticCollectionInit.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ConsoleApp2; + +public static class TestInterleavedStaticCollectionInit +{ + public class ElemClass + { + public int elem; + } + + public class InterlevedTestClass + { + public readonly int bar = 2; + + public readonly List strings; + + public List oOOIntListField; + + public static readonly List staticElemClassListField; + + public static readonly List staticStringListField; + + public List ListProp1 { get; set; } + + public List StringListProp1 { get; set; } + + public static List StaticIntProp1 { get; } + + public InterlevedTestClass() + { + int num = 5; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = "one"; + num2++; + span[num2] = "two"; + num2++; + span[num2] = "three"; + num2++; + span[num2] = "four"; + span[num2 + 1] = "five"; + strings = list; + num2 = 5; + List list2 = new List(num2); + CollectionsMarshal.SetCount(list2, num2); + Span span2 = CollectionsMarshal.AsSpan(list2); + num = 0; + span2[num] = 4; + num++; + span2[num] = 5; + num++; + span2[num] = 6; + num++; + span2[num] = 7; + span2[num + 1] = 8; + ListProp1 = list2; + num = 5; + List list3 = new List(num); + CollectionsMarshal.SetCount(list3, num); + Span span3 = CollectionsMarshal.AsSpan(list3); + num2 = 0; + span3[num2] = 1; + num2++; + span3[num2] = 2; + num2++; + span3[num2] = 3; + num2++; + span3[num2] = 4; + span3[num2 + 1] = 5; + oOOIntListField = list3; + num2 = 5; + List list4 = new List(num2); + CollectionsMarshal.SetCount(list4, num2); + Span span4 = CollectionsMarshal.AsSpan(list4); + num = 0; + span4[num] = "nine"; + num++; + span4[num] = "ten"; + num++; + span4[num] = "eleven"; + num++; + span4[num] = "twelve"; + span4[num + 1] = "thirteen"; + StringListProp1 = list4; + base._002Ector(); + } + + static InterlevedTestClass() + { + int num = 5; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + span[num2] = new ElemClass + { + elem = 1 + }; + num2++; + span[num2] = new ElemClass + { + elem = 2 + }; + num2++; + span[num2] = new ElemClass + { + elem = 3 + }; + num2++; + span[num2] = new ElemClass + { + elem = 4 + }; + num2++; + span[num2] = new ElemClass + { + elem = 5 + }; + staticElemClassListField = list; + num2 = 5; + List list2 = new List(num2); + CollectionsMarshal.SetCount(list2, num2); + Span span2 = CollectionsMarshal.AsSpan(list2); + num = 0; + span2[num] = 1; + num++; + span2[num] = 2; + num++; + span2[num] = 3; + num++; + span2[num] = 4; + num++; + span2[num] = 5; + StaticIntProp1 = list2; + num = 5; + List list3 = new List(num); + CollectionsMarshal.SetCount(list3, num); + Span span3 = CollectionsMarshal.AsSpan(list3); + num2 = 0; + span3[num2] = "one"; + num2++; + span3[num2] = "two"; + num2++; + span3[num2] = "three"; + num2++; + span3[num2] = "four"; + num2++; + span3[num2] = "five"; + staticStringListField = list3; + } + } +} diff --git a/godot-mono-decomp/collection_examples/decompiled/TestNestedCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/decompiled/TestNestedCollectionExpressionInitializers.cs new file mode 100644 index 00000000..d197c3d0 --- /dev/null +++ b/godot-mono-decomp/collection_examples/decompiled/TestNestedCollectionExpressionInitializers.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace ConsoleApp2; + +public static class TestNestedCollectionExpressionInitializers +{ + public class ElemClassWithCollection + { + public int intField; + + public List intListField; + + public int outOfOrderField; + + public string StringProp { get; set; } + } + + public class ParentClassWithCollection + { + public readonly int bar = 2; + + public List elems; + + public ParentClassWithCollection() + { + int num = 5; + List list = new List(num); + CollectionsMarshal.SetCount(list, num); + Span span = CollectionsMarshal.AsSpan(list); + int num2 = 0; + ref ElemClassWithCollection reference = ref span[num2]; + ElemClassWithCollection obj = new ElemClassWithCollection + { + intField = 1 + }; + int num3 = 5; + List list2 = new List(num3); + CollectionsMarshal.SetCount(list2, num3); + Span span2 = CollectionsMarshal.AsSpan(list2); + int num4 = 0; + span2[num4] = 1; + num4++; + span2[num4] = 2; + num4++; + span2[num4] = 3; + num4++; + span2[num4] = 4; + span2[num4 + 1] = 5; + obj.intListField = list2; + obj.StringProp = "string_1"; + obj.outOfOrderField = 2; + reference = obj; + num2++; + ref ElemClassWithCollection reference2 = ref span[num2]; + ElemClassWithCollection obj2 = new ElemClassWithCollection + { + intField = 2 + }; + num4 = 5; + List list3 = new List(num4); + CollectionsMarshal.SetCount(list3, num4); + Span span3 = CollectionsMarshal.AsSpan(list3); + num3 = 0; + span3[num3] = 6; + num3++; + span3[num3] = 7; + num3++; + span3[num3] = 8; + num3++; + span3[num3] = 9; + span3[num3 + 1] = 10; + obj2.intListField = list3; + obj2.StringProp = "string_2"; + obj2.outOfOrderField = 3; + reference2 = obj2; + num2++; + ref ElemClassWithCollection reference3 = ref span[num2]; + ElemClassWithCollection obj3 = new ElemClassWithCollection + { + intField = 3 + }; + num3 = 5; + List list4 = new List(num3); + CollectionsMarshal.SetCount(list4, num3); + Span span4 = CollectionsMarshal.AsSpan(list4); + num4 = 0; + span4[num4] = 11; + num4++; + span4[num4] = 12; + num4++; + span4[num4] = 13; + num4++; + span4[num4] = 14; + span4[num4 + 1] = 15; + obj3.intListField = list4; + obj3.StringProp = "string_3"; + obj3.outOfOrderField = 4; + reference3 = obj3; + num2++; + ref ElemClassWithCollection reference4 = ref span[num2]; + ElemClassWithCollection obj4 = new ElemClassWithCollection + { + intField = 4 + }; + num4 = 5; + List list5 = new List(num4); + CollectionsMarshal.SetCount(list5, num4); + Span span5 = CollectionsMarshal.AsSpan(list5); + num3 = 0; + span5[num3] = 16; + num3++; + span5[num3] = 17; + num3++; + span5[num3] = 18; + num3++; + span5[num3] = 19; + span5[num3 + 1] = 20; + obj4.intListField = list5; + obj4.StringProp = "string_4"; + obj4.outOfOrderField = 5; + reference4 = obj4; + ref ElemClassWithCollection reference5 = ref span[num2 + 1]; + ElemClassWithCollection obj5 = new ElemClassWithCollection + { + intField = 5 + }; + num3 = 5; + List list6 = new List(num3); + CollectionsMarshal.SetCount(list6, num3); + Span span6 = CollectionsMarshal.AsSpan(list6); + num4 = 0; + span6[num4] = 21; + num4++; + span6[num4] = 22; + num4++; + span6[num4] = 23; + num4++; + span6[num4] = 24; + span6[num4 + 1] = 25; + obj5.intListField = list6; + obj5.StringProp = "string_5"; + obj5.outOfOrderField = 6; + reference5 = obj5; + elems = list; + base._002Ector(); + } + } +} diff --git a/godot-mono-decomp/collection_examples/original/TestCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/original/TestCollectionExpressionInitializers.cs new file mode 100644 index 00000000..fada2d7b --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestCollectionExpressionInitializers.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestCollectionExpressionInitializers +{ + + public class TestClassBase + { + + public readonly int parentIntField = 1; + + public readonly List numbers; + + public readonly string secondField; + + public TestClassBase(List numbers) + { + this.numbers = numbers; + this.secondField = "second string"; + } + + } + + public class ElemClass + { + public int elem; + } + public class TestClass1 : TestClassBase + { + + public readonly int bar = 2; + + public readonly List strings = ["one", "two", "three", "four", "five"]; + + public static readonly List elems = [new ElemClass { elem = 1 }, new ElemClass { elem = 2 }, new ElemClass { elem = 3 }, new ElemClass { elem = 4 }, new ElemClass { elem = 5 }]; + + public readonly Dictionary dict = new Dictionary { ["one"] = 1, ["two"] = 2, ["three"] = 3, ["four"] = 4, ["five"] = 5 }; + + public int randomField; + + public string anotherRandomField; + + public List fieldInitializedInConstructor; + + public int IntProp1 { get; set; } = 1; + + public List ListProp1 { get; set; } = [4,5,6,7,8]; + + public int IntProp2 { get; set; } = 2; + + // field initialized after properties + public readonly HashSet set = [1, 2, 3, 4, 5]; + + public List StringListProp1 { get; set; } = ["nine", "ten", "eleven", "twelve", "thirteen"]; + + public TestClass1() : base([1, 2, 3, 4, 5]) + { + foo(); + fieldInitializedInConstructor = [ + "string_1", + "string_2", + "string_3", + "string_4", + "string_5" + ]; + anotherRandomField = "another string"; + + } + + void foo(){ + randomField = 1; + + } + } +} diff --git a/godot-mono-decomp/collection_examples/original/TestCollectionInitWithSpread.cs b/godot-mono-decomp/collection_examples/original/TestCollectionInitWithSpread.cs new file mode 100644 index 00000000..a6d062b6 --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestCollectionInitWithSpread.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestCollectionInitWithSpread +{ + + private static IEnumerable stringListConst1 => [ "one", "two", "three", "four", "five" ]; + + private static IEnumerable stringListConst2 => ["six", "seven", "eight", "nine", "ten" ]; + + private static IEnumerable stringListConst3 => ["eleven", "twelve", "thirteen", "fourteen", "fifteen"]; + + + public class TestStaticHashSetMemberSpreadInit + { + public static readonly HashSet strings = [..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]; + + } + + + public class TestStaticListMemberSpreadInit + { + public static readonly List strings = [..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]; + + } + + + public class TestStaticReadOnlySetMemberSpreadInit + { + + public static readonly IReadOnlySet strings = new HashSet([..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]); + + } + public class TestStaticReadOnlyListMemberSpreadInit + { + public static readonly IReadOnlyList strings = [..stringListConst1, ..stringListConst2, ..stringListConst3, "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]; + + } + +} diff --git a/godot-mono-decomp/collection_examples/original/TestCtorBoundaryCoverage.cs b/godot-mono-decomp/collection_examples/original/TestCtorBoundaryCoverage.cs new file mode 100644 index 00000000..6c15b0f8 --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestCtorBoundaryCoverage.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestCtorBoundaryCoverage +{ + public class BoundaryBase + { + public readonly List baseNumbers; + + public BoundaryBase(List baseNumbers) + { + this.baseNumbers = baseNumbers; + } + } + + public class TailBoundaryDerived : BoundaryBase + { + public readonly List names = ["alpha", "beta", "gamma"]; + + public TailBoundaryDerived() + : base([1, 2, 3]) + { + } + } + + public class TransitionBoundaryDerived : BoundaryBase + { + public readonly List values = [10, 20, 30]; + + public string data; + + public TransitionBoundaryDerived() + : base([4, 5, 6]) + { + data = ComputeData(); + } + + private static string ComputeData() + { + return "from-function"; + } + } +} diff --git a/godot-mono-decomp/collection_examples/original/TestFuncInitializer.cs b/godot-mono-decomp/collection_examples/original/TestFuncInitializer.cs new file mode 100644 index 00000000..7c1b576d --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestFuncInitializer.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestFuncInitializer +{ + + public class TestClass1 + { + + public readonly List strings = ["one", "two", "three", "four", "five"]; + private Func filter = (string s) => s.Length > 3; + } + +} \ No newline at end of file diff --git a/godot-mono-decomp/collection_examples/original/TestInterleavedStaticCollectionInit.cs b/godot-mono-decomp/collection_examples/original/TestInterleavedStaticCollectionInit.cs new file mode 100644 index 00000000..78e50469 --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestInterleavedStaticCollectionInit.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestInterleavedStaticCollectionInit +{ + + public class ElemClass + { + public int elem; + } + public class InterlevedTestClass + { + + public readonly int bar = 2; + + public readonly List strings = ["one", "two", "three", "four", "five"]; + + + public List ListProp1 { get; set; } = [4,5,6,7,8]; + + public List oOOIntListField = [1, 2, 3, 4, 5]; + + public List StringListProp1 { get; set; } = ["nine", "ten", "eleven", "twelve", "thirteen"]; + + + public static readonly List staticElemClassListField = [new ElemClass { elem = 1 }, new ElemClass { elem = 2 }, new ElemClass { elem = 3 }, new ElemClass { elem = 4 }, new ElemClass { elem = 5 }]; + + public static List StaticIntProp1 { get; } = [1, 2, 3, 4, 5]; + public static readonly List staticStringListField = ["one", "two", "three", "four", "five"]; + } +} diff --git a/godot-mono-decomp/collection_examples/original/TestNestedCollectionExpressionInitializers.cs b/godot-mono-decomp/collection_examples/original/TestNestedCollectionExpressionInitializers.cs new file mode 100644 index 00000000..6350a85c --- /dev/null +++ b/godot-mono-decomp/collection_examples/original/TestNestedCollectionExpressionInitializers.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace ConsoleApp2; + +public static class TestNestedCollectionExpressionInitializers +{ + + + public class ElemClassWithCollection + { + public int intField; + public List intListField; + + public string StringProp { get; set; } + + public int outOfOrderField; + + } + + // example of a class that has a list of objects that are initialized with collection expressions + public class ParentClassWithCollection + { + + public readonly int bar = 2; + + public List elems =[ + new ElemClassWithCollection { intField = 1, intListField = [1, 2, 3, 4, 5], StringProp = "string_1", outOfOrderField = 2 }, + new ElemClassWithCollection { intField = 2, intListField = [6, 7, 8, 9, 10], StringProp = "string_2", outOfOrderField = 3 }, + new ElemClassWithCollection { intField = 3, intListField = [11, 12, 13, 14, 15], StringProp = "string_3", outOfOrderField = 4 }, + new ElemClassWithCollection { intField = 4, intListField = [16, 17, 18, 19, 20], StringProp = "string_4", outOfOrderField = 5 }, + new ElemClassWithCollection { intField = 5, intListField = [21, 22, 23, 24, 25], StringProp = "string_5", outOfOrderField = 6 }, + ]; + } +} diff --git a/godot-mono-decomp/collection_examples/validation/.gitignore b/godot-mono-decomp/collection_examples/validation/.gitignore new file mode 100644 index 00000000..fb635bcd --- /dev/null +++ b/godot-mono-decomp/collection_examples/validation/.gitignore @@ -0,0 +1,2 @@ +decompiled_out/ +realworld_test_out/ diff --git a/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj b/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj new file mode 100644 index 00000000..3ec47c36 --- /dev/null +++ b/godot-mono-decomp/collection_examples/validation/CollectionExamplesValidation.csproj @@ -0,0 +1,18 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + + diff --git a/godot-mono-decomp/collection_examples/validation/test_sts2_realworld.sh b/godot-mono-decomp/collection_examples/validation/test_sts2_realworld.sh new file mode 100755 index 00000000..d87531c0 --- /dev/null +++ b/godot-mono-decomp/collection_examples/validation/test_sts2_realworld.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +CLI_PROJECT="$REPO_DIR/GodotMonoDecompCLI/GodotMonoDecompCLI.csproj" + +DEFAULT_DLL_PATH="/Users/nikita/Library/Application Support/Steam/steamapps/common/Slay the Spire 2/SlayTheSpire2.app/Contents/Resources/data_sts2_macos_arm64/sts2.dll" +DEFAULT_OUTPUT_ROOT="$SCRIPT_DIR/realworld_test_out" + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + echo "Usage: $0 [sts2_dll_path] [output_root_dir]" + echo "Defaults:" + echo " sts2_dll_path $DEFAULT_DLL_PATH" + echo " output_root_dir $DEFAULT_OUTPUT_ROOT" + exit 0 +fi + +STS2_DLL_PATH="${1:-$DEFAULT_DLL_PATH}" +OUTPUT_ROOT="${2:-$DEFAULT_OUTPUT_ROOT}" + +if [[ ! -f "$STS2_DLL_PATH" ]]; then + echo "Error: STS2 DLL not found at: $STS2_DLL_PATH" + exit 1 +fi + +echo "[1/6] Building decompiler CLI" +dotnet build "$CLI_PROJECT" + +NO_LIFT_OUT="$OUTPUT_ROOT/no_lift" +LIFT_OUT="$OUTPUT_ROOT/lift" +NO_LIFT_PROJECT="SlayTheSpire2.NoLift" +LIFT_PROJECT="SlayTheSpire2.Lift" + +echo "[2/6] Decompiling STS2 with lifting disabled" +mkdir -p "$NO_LIFT_OUT" +dotnet run --project "$CLI_PROJECT" -- "$STS2_DLL_PATH" \ + --output-dir "$NO_LIFT_OUT" \ + --project-name "$NO_LIFT_PROJECT" \ + --disable-collection-initializer-lifting + +echo "[3/6] Building no-lift decompiled project" +dotnet build "$NO_LIFT_OUT/$NO_LIFT_PROJECT.csproj" /p:WarningLevel=0 + +echo "[4/6] Decompiling STS2 with lifting enabled" +mkdir -p "$LIFT_OUT" +dotnet run --project "$CLI_PROJECT" -- "$STS2_DLL_PATH" \ + --output-dir "$LIFT_OUT" \ + --project-name "$LIFT_PROJECT" \ + --enable-collection-initializer-lifting + +echo "[5/6] Building lift-enabled decompiled project" +dotnet build "$LIFT_OUT/$LIFT_PROJECT.csproj" /p:WarningLevel=0 + +echo "[6/6] Real-world STS2 decompile validation passed" +echo "No-lift output: $NO_LIFT_OUT" +echo "Lift output: $LIFT_OUT" diff --git a/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh b/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh new file mode 100755 index 00000000..0e213634 --- /dev/null +++ b/godot-mono-decomp/collection_examples/validation/validate_collection_lifting.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +VALIDATION_PROJECT="$SCRIPT_DIR/CollectionExamplesValidation.csproj" +ASSEMBLY_PATH="$SCRIPT_DIR/bin/Debug/net10.0/CollectionExamplesValidation.dll" +CLI_PROJECT="$REPO_DIR/GodotMonoDecompCLI/GodotMonoDecompCLI.csproj" +ORIGINAL_PROJECT_DIR="$REPO_DIR/collection_examples/original" +BASELINE_CTOR_FILE="$REPO_DIR/collection_examples/decompiled/TestCtorBoundaryCoverage.cs" +UPDATE_FIXTURES=true + +for arg in "$@"; do + case "$arg" in + --update-fixtures) + UPDATE_FIXTURES=true + ;; + --no-update-fixtures) + UPDATE_FIXTURES=false + ;; + -h|--help) + echo "Usage: $0 [--update-fixtures|--no-update-fixtures]" + echo " --update-fixtures Write output to validation/decompiled_out (default)" + echo " --no-update-fixtures Write output to a temporary directory" + exit 0 + ;; + *) + echo "Unknown argument: $arg" + echo "Use --help for usage." + exit 2 + ;; + esac +done + +if $UPDATE_FIXTURES; then + OUTPUT_DIR="$SCRIPT_DIR/decompiled_out" + echo "Using tracked output directory: $OUTPUT_DIR" +else + OUTPUT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/collection-lifting-XXXXXX")" + echo "Using temporary output directory: $OUTPUT_DIR" + trap 'rm -rf "$OUTPUT_DIR"' EXIT +fi + +echo "[1/7] Baseline ILSpy sanity checks" +test -f "$BASELINE_CTOR_FILE" +grep -q "base._002Ector(" "$BASELINE_CTOR_FILE" +grep -q "CollectionsMarshal.SetCount" "$BASELINE_CTOR_FILE" +grep -q "data = ComputeData();" "$BASELINE_CTOR_FILE" + +echo "[2/7] Building validation input assembly" +dotnet build "$VALIDATION_PROJECT" + +echo "[3/7] Running decompiler CLI" +echo "Output directory: $OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" +if $UPDATE_FIXTURES; then + rm -f "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" \ + "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" \ + "$OUTPUT_DIR/TestFuncInitializer.cs" \ + "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" \ + "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" \ + "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" \ + "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.csproj" \ + "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.sln" +fi + +dotnet run --project "$CLI_PROJECT" -- "$ASSEMBLY_PATH" \ + --output-dir "$OUTPUT_DIR" \ + --project-name "CollectionExamplesValidation.Decompiled" \ + --extracted-project "$ORIGINAL_PROJECT_DIR" \ + --enable-collection-initializer-lifting + +echo "[4/7] Asserting expected output files exist" +test -f "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" +test -f "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" +test -f "$OUTPUT_DIR/TestFuncInitializer.cs" +test -f "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" +test -f "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" +test -f "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +test -f "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.csproj" + +echo "[5/7] Asserting expected lifted and preserved markers" +grep -q "public readonly List strings = new List" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" +grep -q "public static readonly List elems = new List" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" +grep -q "public List ListProp1 { get; set; } = new List" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" +grep -q "public readonly HashSet set = new HashSet { 1, 2, 3, 4, 5 };" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" +grep -q "foo();" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" +grep -q "public TailBoundaryDerived()" "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" +grep -q ": base(new List { 1, 2, 3 })" "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" +grep -q "public TransitionBoundaryDerived()" "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" +grep -q "data = ComputeData();" "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" +grep -q "public List elems = new List" "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" +grep -q "intListField = new List" "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" +grep -q "public static readonly List staticElemClassListField = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +grep -q "public static List StaticIntProp1 { get; } = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +grep -q "public static readonly List staticStringListField = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +grep -q "public readonly List strings = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +grep -q "public List ListProp1 { get; set; } = new List" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs" +grep -q "private Func filter = (string s) => s.Length > 3;" "$OUTPUT_DIR/TestFuncInitializer.cs" +grep -Fq "public static readonly HashSet strings = new HashSet([..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" +grep -Fq "public static readonly List strings = new List([..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" +grep -Fq "public static readonly IReadOnlySet strings = new HashSet([..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" +grep -Fq "public static readonly IReadOnlyList strings = [..stringListConst1" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" +line_intprop1="$(grep -n "public int IntProp1 { get; set; } = 1;" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" | awk -F: 'NR==1 {print $1}')" +line_listprop1="$(grep -n "public List ListProp1 { get; set; } = new List" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" | awk -F: 'NR==1 {print $1}')" +line_intprop2="$(grep -n "public int IntProp2 { get; set; } = 2;" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" | awk -F: 'NR==1 {print $1}')" +line_set="$(grep -n "public readonly HashSet set = new HashSet { 1, 2, 3, 4, 5 };" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" | awk -F: 'NR==1 {print $1}')" +if [ -z "$line_intprop1" ] || [ -z "$line_listprop1" ] || [ -z "$line_intprop2" ] || [ -z "$line_set" ] \ + || [ "$line_intprop1" -ge "$line_listprop1" ] || [ "$line_listprop1" -ge "$line_intprop2" ] || [ "$line_intprop2" -ge "$line_set" ]; then + echo "Unexpected TestClass1 declaration order: expected IntProp1 -> ListProp1 -> IntProp2 -> set." + exit 1 +fi +if grep -q "ref ElemClassWithCollection reference" "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs"; then + echo "Found unexpected imperative nested ref-slot assignments in decompiled nested output." + exit 1 +fi +if grep -q "AddRange(" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" \ + || grep -q "foreach (string item" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs"; then + echo "Found unexpected spread prelude statements in decompiled spread output." + exit 1 +fi +if grep -q "new _003C_003Ez__ReadOnlyList(new List(\\[\\." "$OUTPUT_DIR/TestCollectionInitWithSpread.cs"; then + echo "Found unexpected redundant ReadOnlyList(List(collection-expression)) wrapper." + exit 1 +fi + +echo "[6/7] Asserting removed ctor-artifact markers" +if grep -q "_002Ector(" "$OUTPUT_DIR/TestCollectionExpressionInitializers.cs" \ + || grep -q "_002Ector(" "$OUTPUT_DIR/TestCollectionInitWithSpread.cs" \ + || grep -q "_002Ector(" "$OUTPUT_DIR/TestFuncInitializer.cs" \ + || grep -q "_002Ector(" "$OUTPUT_DIR/TestNestedCollectionExpressionInitializers.cs" \ + || grep -q "_002Ector(" "$OUTPUT_DIR/TestCtorBoundaryCoverage.cs" \ + || grep -q "_002Ector(" "$OUTPUT_DIR/TestInterleavedStaticCollectionInit.cs"; then + echo "Found unexpected _002Ector(...) artifact in decompiled output." + exit 1 +fi + +echo "[7/7] Building decompiled project" +dotnet build "$OUTPUT_DIR/CollectionExamplesValidation.Decompiled.csproj" + +echo "Validation passed." diff --git a/godot-mono-decomp/dependencies/ICSharpCode.Decompiler.10.0.0.8290-preview3.nupkg b/godot-mono-decomp/dependencies/ICSharpCode.Decompiler.10.0.0.8290-preview3.nupkg new file mode 100644 index 00000000..0750cab1 Binary files /dev/null and b/godot-mono-decomp/dependencies/ICSharpCode.Decompiler.10.0.0.8290-preview3.nupkg differ diff --git a/godot-mono-decomp/dependencies/ICSharpCode.ILSpyX.10.0.0.8290-preview3.nupkg b/godot-mono-decomp/dependencies/ICSharpCode.ILSpyX.10.0.0.8290-preview3.nupkg new file mode 100644 index 00000000..50fa0aa8 Binary files /dev/null and b/godot-mono-decomp/dependencies/ICSharpCode.ILSpyX.10.0.0.8290-preview3.nupkg differ diff --git a/godot-mono-decomp/nuget.config b/godot-mono-decomp/nuget.config index 6b267fba..09eb9895 100644 --- a/godot-mono-decomp/nuget.config +++ b/godot-mono-decomp/nuget.config @@ -1,5 +1,5 @@ - + diff --git a/utility/file_access_gdre.cpp b/utility/file_access_gdre.cpp index d2eb5a5c..b0c4c515 100644 --- a/utility/file_access_gdre.cpp +++ b/utility/file_access_gdre.cpp @@ -194,7 +194,7 @@ void GDREPackedData::add_path(const String &p_pkg_path, const String &p_path, ui Ref pf_info; pf_info.instantiate(); String abs_path = p_path.is_relative_path() ? "res://" + p_path : p_path; - pf_info->init(abs_path, &pf); + pf_info->init(abs_path, &pf, p_src == &dummy_source); // Get the fixed path if this is from a PCK source String path = p_pck_src ? pf_info->get_path() : abs_path.simplify_path(); diff --git a/utility/packed_file_info.cpp b/utility/packed_file_info.cpp index d39f402a..397d437d 100644 --- a/utility/packed_file_info.cpp +++ b/utility/packed_file_info.cpp @@ -13,6 +13,7 @@ void PackedFileInfo::_bind_methods() { ClassDB::bind_method(D_METHOD("is_malformed"), &PackedFileInfo::is_malformed); ClassDB::bind_method(D_METHOD("is_encrypted"), &PackedFileInfo::is_encrypted); ClassDB::bind_method(D_METHOD("is_checksum_validated"), &PackedFileInfo::is_checksum_validated); + ClassDB::bind_method(D_METHOD("is_dummy"), &PackedFileInfo::is_dummy); } #define PATH_REPLACER "_" diff --git a/utility/packed_file_info.h b/utility/packed_file_info.h index 7198f0b7..72539a9e 100644 --- a/utility/packed_file_info.h +++ b/utility/packed_file_info.h @@ -19,14 +19,16 @@ class PackedFileInfo : public RefCounted { bool malformed_path; bool md5_passed = false; uint32_t flags; + bool dummy_path = false; void set_md5_match(bool pass) { md5_passed = pass; } public: - void init(const String &p_path, const PackedData::PackedFile *pfstruct) { + void init(const String &p_path, const PackedData::PackedFile *pfstruct, bool p_is_dummy = false) { pf = *pfstruct; raw_path = p_path; malformed_path = false; + dummy_path = p_is_dummy; fix_path(); } void init(const String &pck_path, const String &p_path, const uint64_t ofs, const uint64_t sz, const uint8_t md5arr[16], PackSource *p_src, const bool encrypted = false) { @@ -65,10 +67,13 @@ class PackedFileInfo : public RefCounted { bool is_checksum_validated() const { return md5_passed; } + bool is_dummy() const { + return dummy_path; + } protected: static void _bind_methods(); private: void fix_path(); -}; \ No newline at end of file +}; diff --git a/utility/pck_dumper.cpp b/utility/pck_dumper.cpp index 95689ab8..fb1563ac 100644 --- a/utility/pck_dumper.cpp +++ b/utility/pck_dumper.cpp @@ -192,6 +192,9 @@ Error PckDumper::_pck_dump_to_dir( gdre::CaselessHashSet seen_paths; HashSet files_to_extract_set = gdre::vector_to_hashset(files_to_extract); for (int i = 0; i < files.size(); i++) { + if (files.get(i)->is_dummy()) { + continue; + } const auto &file = files.get(i); String path = file->get_path(); if (!files_to_extract_set.is_empty() && !files_to_extract_set.has(path)) {