Skip to content

Commit 37c56e7

Browse files
Merge pull request #3449 from ds5678/fix-nested-namespace-directories
Fix nested namespace directories
2 parents b1a617c + 8439e1c commit 37c56e7

File tree

5 files changed

+130
-17
lines changed

5 files changed

+130
-17
lines changed

ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
<Compile Include="Helpers\TestsAssemblyOutput.cs" />
132132
<Compile Include="Output\InsertParenthesesVisitorTests.cs" />
133133
<Compile Include="ProjectDecompiler\TargetFrameworkTests.cs" />
134+
<Compile Include="ProjectDecompiler\WholeProjectDecompilerTests.cs" />
134135
<Compile Include="TestAssemblyResolver.cs" />
135136
<Compile Include="TestCases\ILPretty\Issue3421.cs" />
136137
<Compile Include="TestCases\ILPretty\Issue3442.cs" />

ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
using NUnit.Framework;
2525

26-
namespace ICSharpCode.Decompiler.Tests
26+
namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler
2727
{
2828
[TestFixture]
2929
public sealed class TargetFrameworkTests
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) 2025 Daniel Grunwald
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
// software and associated documentation files (the "Software"), to deal in the Software
5+
// without restriction, including without limitation the rights to use, copy, modify, merge,
6+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7+
// to whom the Software is furnished to do so, subject to the following conditions:
8+
//
9+
// The above copyright notice and this permission notice shall be included in all copies or
10+
// substantial portions of the Software.
11+
//
12+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
14+
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
15+
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
17+
// DEALINGS IN THE SOFTWARE.
18+
19+
using System;
20+
using System.Collections.Generic;
21+
using System.IO;
22+
23+
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
24+
using ICSharpCode.Decompiler.Metadata;
25+
26+
using NUnit.Framework;
27+
28+
namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler;
29+
30+
[TestFixture]
31+
public sealed class WholeProjectDecompilerTests
32+
{
33+
[Test]
34+
public void UseNestedDirectoriesForNamespacesTrueWorks()
35+
{
36+
string targetDirectory = Path.Combine(Environment.CurrentDirectory, Path.GetRandomFileName());
37+
TestFriendlyProjectDecompiler decompiler = new(new UniversalAssemblyResolver(null, false, null));
38+
decompiler.Settings.UseNestedDirectoriesForNamespaces = true;
39+
decompiler.DecompileProject(new PEFile("ICSharpCode.Decompiler.dll"), targetDirectory);
40+
AssertDirectoryDoesntExist(targetDirectory);
41+
42+
string projectDecompilerDirectory = Path.Combine(targetDirectory, "ICSharpCode", "Decompiler", "CSharp", "ProjectDecompiler");
43+
string projectDecompilerFile = Path.Combine(projectDecompilerDirectory, $"{nameof(WholeProjectDecompiler)}.cs");
44+
45+
using (Assert.EnterMultipleScope())
46+
{
47+
Assert.That(decompiler.Files.ContainsKey(projectDecompilerFile), Is.True);
48+
Assert.That(decompiler.Directories.Contains(projectDecompilerDirectory), Is.True);
49+
}
50+
}
51+
52+
[Test]
53+
public void UseNestedDirectoriesForNamespacesFalseWorks()
54+
{
55+
string targetDirectory = Path.Combine(Environment.CurrentDirectory, Path.GetRandomFileName());
56+
TestFriendlyProjectDecompiler decompiler = new(new UniversalAssemblyResolver(null, false, null));
57+
decompiler.Settings.UseNestedDirectoriesForNamespaces = false;
58+
decompiler.DecompileProject(new PEFile("ICSharpCode.Decompiler.dll"), targetDirectory);
59+
AssertDirectoryDoesntExist(targetDirectory);
60+
61+
string projectDecompilerDirectory = Path.Combine(targetDirectory, "ICSharpCode.Decompiler.CSharp.ProjectDecompiler");
62+
string projectDecompilerFile = Path.Combine(projectDecompilerDirectory, $"{nameof(WholeProjectDecompiler)}.cs");
63+
64+
using (Assert.EnterMultipleScope())
65+
{
66+
Assert.That(decompiler.Files.ContainsKey(projectDecompilerFile), Is.True);
67+
Assert.That(decompiler.Directories.Contains(projectDecompilerDirectory), Is.True);
68+
}
69+
}
70+
71+
static void AssertDirectoryDoesntExist(string directory)
72+
{
73+
if (Directory.Exists(directory))
74+
{
75+
Directory.Delete(directory, recursive: true);
76+
Assert.Fail("Directory should not have been created.");
77+
}
78+
}
79+
80+
sealed class TestFriendlyProjectDecompiler(IAssemblyResolver assemblyResolver) : WholeProjectDecompiler(assemblyResolver)
81+
{
82+
public Dictionary<string, StringWriter> Files { get; } = [];
83+
public HashSet<string> Directories { get; } = [];
84+
85+
protected override TextWriter CreateFile(string path)
86+
{
87+
StringWriter writer = new();
88+
Files[path] = writer;
89+
return writer;
90+
}
91+
92+
protected override void CreateDirectory(string path)
93+
{
94+
Directories.Add(path);
95+
}
96+
97+
protected override IEnumerable<ProjectItemInfo> WriteMiscellaneousFilesInProject(PEFile module) => [];
98+
99+
protected override IEnumerable<ProjectItemInfo> WriteResourceFilesInProject(MetadataFile module) => [];
100+
}
101+
}

ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ protected WholeProjectDecompiler(
137137
public void DecompileProject(MetadataFile file, string targetDirectory, CancellationToken cancellationToken = default(CancellationToken))
138138
{
139139
string projectFileName = Path.Combine(targetDirectory, CleanUpFileName(file.Name, ".csproj"));
140-
using (var writer = new StreamWriter(projectFileName))
140+
using (var writer = CreateFile(projectFileName))
141141
{
142142
DecompileProject(file, targetDirectory, writer, cancellationToken);
143143
}
@@ -186,6 +186,24 @@ protected virtual bool IncludeTypeWhenDecompilingProject(MetadataFile module, Ty
186186
return true;
187187
}
188188

189+
protected virtual TextWriter CreateFile(string path)
190+
{
191+
return new StreamWriter(path);
192+
}
193+
194+
protected virtual void CreateDirectory(string path)
195+
{
196+
try
197+
{
198+
Directory.CreateDirectory(path);
199+
}
200+
catch (IOException)
201+
{
202+
File.Delete(path);
203+
Directory.CreateDirectory(path);
204+
}
205+
}
206+
189207
CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts)
190208
{
191209
var decompiler = new CSharpDecompiler(ts, Settings);
@@ -204,9 +222,9 @@ IEnumerable<ProjectItemInfo> WriteAssemblyInfo(DecompilerTypeSystem ts, Cancella
204222

205223
const string prop = "Properties";
206224
if (directories.Add(prop))
207-
Directory.CreateDirectory(Path.Combine(TargetDirectory, prop));
225+
CreateDirectory(Path.Combine(TargetDirectory, prop));
208226
string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs");
209-
using (StreamWriter w = new StreamWriter(Path.Combine(TargetDirectory, assemblyInfo)))
227+
using (var w = CreateFile(Path.Combine(TargetDirectory, assemblyInfo)))
210228
{
211229
syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions));
212230
}
@@ -251,15 +269,7 @@ string GetFileFileNameForHandle(TypeDefinitionHandle h)
251269
if (directories.Add(dir))
252270
{
253271
var path = Path.Combine(TargetDirectory, dir);
254-
try
255-
{
256-
Directory.CreateDirectory(path);
257-
}
258-
catch (IOException)
259-
{
260-
File.Delete(path);
261-
Directory.CreateDirectory(path);
262-
}
272+
CreateDirectory(path);
263273
}
264274
return Path.Combine(dir, file);
265275
}
@@ -277,7 +287,7 @@ void ProcessFiles(List<IGrouping<string, TypeDefinitionHandle>> files)
277287
delegate (IGrouping<string, TypeDefinitionHandle> file) {
278288
try
279289
{
280-
using StreamWriter w = new StreamWriter(Path.Combine(TargetDirectory, file.Key));
290+
using var w = CreateFile(Path.Combine(TargetDirectory, file.Key));
281291
CSharpDecompiler decompiler = CreateDecompiler(ts);
282292

283293
foreach (var partialType in partialTypes)
@@ -344,7 +354,7 @@ protected virtual IEnumerable<ProjectItemInfo> WriteResourceFilesInProject(Metad
344354
string dirName = Path.GetDirectoryName(fileName);
345355
if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName))
346356
{
347-
Directory.CreateDirectory(Path.Combine(TargetDirectory, dirName));
357+
CreateDirectory(Path.Combine(TargetDirectory, dirName));
348358
}
349359
Stream entryStream = (Stream)value;
350360
entryStream.Position = 0;
@@ -743,7 +753,8 @@ public static string CleanUpDirectoryName(string text)
743753

744754
public static string CleanUpPath(string text)
745755
{
746-
return CleanUpName(text, separateAtDots: true, treatAsFileName: true, treatAsPath: true);
756+
return CleanUpName(text, separateAtDots: true, treatAsFileName: false, treatAsPath: true)
757+
.Replace('.', Path.DirectorySeparatorChar);
747758
}
748759

749760
static bool IsReservedFileSystemName(string name)

ILSpy.BamlDecompiler/BamlResourceNodeFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public string WriteResourceToFile(LoadedAssembly assembly, string fileName, Stre
6262
var typeDefinition = result.TypeName.HasValue ? typeSystem.MainModule.GetTypeDefinition(result.TypeName.Value.TopLevelTypeName) : null;
6363
if (typeDefinition != null)
6464
{
65-
fileName = WholeProjectDecompiler.CleanUpPath(typeDefinition.ReflectionName + ".xaml");
65+
fileName = WholeProjectDecompiler.SanitizeFileName(typeDefinition.ReflectionName + ".xaml");
6666
var partialTypeInfo = new PartialTypeInfo(typeDefinition);
6767
foreach (var member in result.GeneratedMembers)
6868
{

0 commit comments

Comments
 (0)