Skip to content

Commit ac597a5

Browse files
authored
Add support for file-scoped namespaces, #113 (#115)
This change surfaced some annoying whitespace behavior with Roslyn's NormalizeWhitespace method, so this adds some leading/trailing newlines in places after NormalizeWhitespace is called to help prettify the output.
1 parent af6ab36 commit ac597a5

File tree

14 files changed

+109
-48
lines changed

14 files changed

+109
-48
lines changed

JavaToCSharp/CommentsHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ static SyntaxNode InsertEmptyLineBeforeComment(SyntaxNode node)
448448
}
449449

450450
node = statement.InsertTriviaBefore(leading[index],
451-
Enumerable.Repeat(SyntaxFactory.CarriageReturnLineFeed, 1));
451+
Enumerable.Repeat(Whitespace.NewLine, 1));
452452
}
453453
}
454454

JavaToCSharp/Declarations/ConstructorDeclarationVisitor.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using com.github.javaparser.ast.stmt;
44
using com.github.javaparser.ast.type;
55
using JavaToCSharp.Statements;
6-
using Microsoft.CodeAnalysis;
76
using Microsoft.CodeAnalysis.CSharp;
87
using Microsoft.CodeAnalysis.CSharp.Syntax;
98

@@ -24,8 +23,7 @@ public class ConstructorDeclarationVisitor : BodyDeclarationVisitor<ConstructorD
2423
return null;
2524
}
2625

27-
var ctorSyntax = SyntaxFactory.ConstructorDeclaration(identifier)
28-
.WithLeadingTrivia(SyntaxFactory.CarriageReturnLineFeed);
26+
var ctorSyntax = SyntaxFactory.ConstructorDeclaration(identifier).WithLeadingNewLines();
2927

3028
var mods = ctorDecl.getModifiers().ToModifierKeywordSet();
3129

JavaToCSharp/Extensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Diagnostics.CodeAnalysis;
22
using java.util;
33
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
45
using Microsoft.CodeAnalysis.CSharp.Syntax;
56
using JavaAst = com.github.javaparser.ast;
67

@@ -77,4 +78,12 @@ public static T FromRequiredOptional<T>(this Optional optional)
7778
public static ISet<JavaAst.Modifier.Keyword> ToModifierKeywordSet(this JavaAst.NodeList nodeList)
7879
=> nodeList.ToList<JavaAst.Modifier>()?.Select(i => i.getKeyword()).ToHashSet()
7980
?? new HashSet<JavaAst.Modifier.Keyword>();
81+
82+
public static TSyntax WithLeadingNewLines<TSyntax>(this TSyntax syntax, int count = 1)
83+
where TSyntax : SyntaxNode
84+
=> syntax.WithLeadingTrivia(Enumerable.Repeat(Whitespace.NewLine, count));
85+
86+
public static TSyntax WithTrailingNewLines<TSyntax>(this TSyntax syntax, int count = 1)
87+
where TSyntax : SyntaxNode
88+
=> syntax.WithTrailingTrivia(Enumerable.Repeat(Whitespace.NewLine, count));
8089
}

JavaToCSharp/JavaConversionOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class JavaConversionOptions
3131

3232
public bool IncludeComments { get; set; } = true;
3333

34+
public bool UseFileScopedNamespaces { get; set; }
35+
3436
public ConversionState ConversionState { get; set; }
3537

3638
public JavaConversionOptions AddPackageReplacement(string pattern, string replacement, RegexOptions options = RegexOptions.None)

JavaToCSharp/JavaToCSharpConverter.cs

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public static class JavaToCSharpConverter
5454
var package = result.getPackageDeclaration().FromOptional<PackageDeclaration>();
5555

5656
var rootMembers = new List<MemberDeclarationSyntax>();
57-
NamespaceDeclarationSyntax? namespaceSyntax = null;
57+
NameSyntax? namespaceNameSyntax = null;
5858

5959
if (options.IncludeNamespace)
6060
{
@@ -72,7 +72,7 @@ public static class JavaToCSharpConverter
7272

7373
packageName = TypeHelper.Capitalize(packageName);
7474

75-
namespaceSyntax = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(packageName));
75+
namespaceNameSyntax = SyntaxFactory.ParseName(packageName);
7676
}
7777

7878
foreach (var type in types)
@@ -82,57 +82,55 @@ public static class JavaToCSharpConverter
8282
if (classOrIntType.isInterface())
8383
{
8484
var interfaceSyntax = ClassOrInterfaceDeclarationVisitor.VisitInterfaceDeclaration(context, classOrIntType);
85-
86-
if (namespaceSyntax != null)
87-
{
88-
namespaceSyntax = namespaceSyntax.AddMembers(interfaceSyntax);
89-
}
90-
else
91-
{
92-
rootMembers.Add(interfaceSyntax);
93-
}
85+
rootMembers.Add(interfaceSyntax.NormalizeWhitespace().WithTrailingNewLines());
9486
}
9587
else
9688
{
9789
var classSyntax = ClassOrInterfaceDeclarationVisitor.VisitClassDeclaration(context, classOrIntType);
98-
99-
if (namespaceSyntax != null)
100-
{
101-
namespaceSyntax = namespaceSyntax.AddMembers(classSyntax);
102-
}
103-
else
104-
{
105-
rootMembers.Add(classSyntax);
106-
}
90+
rootMembers.Add(classSyntax.NormalizeWhitespace().WithTrailingNewLines());
10791
}
10892
}
10993
else if (type is EnumDeclaration enumType)
11094
{
111-
var classSyntax = EnumDeclarationVisitor.VisitEnumDeclaration(context, enumType);
95+
var enumSyntax = EnumDeclarationVisitor.VisitEnumDeclaration(context, enumType);
96+
rootMembers.Add(enumSyntax.NormalizeWhitespace().WithTrailingNewLines());
97+
}
98+
}
11299

113-
if (namespaceSyntax != null)
114-
{
115-
namespaceSyntax = namespaceSyntax.AddMembers(classSyntax);
116-
}
117-
else
118-
{
119-
rootMembers.Add(classSyntax);
120-
}
100+
if (rootMembers.Count > 1)
101+
{
102+
for (int i = 1; i < rootMembers.Count; i++)
103+
{
104+
rootMembers[i] = rootMembers[i].WithLeadingNewLines();
121105
}
122106
}
123107

124-
if (namespaceSyntax != null)
108+
if (namespaceNameSyntax != null)
125109
{
126-
rootMembers.Add(namespaceSyntax);
110+
if (options.UseFileScopedNamespaces && rootMembers.Count > 0)
111+
{
112+
rootMembers[0] = rootMembers[0].WithLeadingNewLines();
113+
}
114+
115+
MemberDeclarationSyntax namespaceSyntax =
116+
options.UseFileScopedNamespaces
117+
? SyntaxFactory.FileScopedNamespaceDeclaration(namespaceNameSyntax)
118+
.NormalizeWhitespace()
119+
.WithTrailingNewLines()
120+
.WithMembers(SyntaxFactory.List(rootMembers))
121+
: SyntaxFactory.NamespaceDeclaration(namespaceNameSyntax)
122+
.WithMembers(SyntaxFactory.List(rootMembers))
123+
.NormalizeWhitespace();
124+
125+
rootMembers = [namespaceSyntax];
127126
}
128127

129128
var root = SyntaxFactory.CompilationUnit(
130129
externs: [],
131-
usings: SyntaxFactory.List(UsingsHelper.GetUsings(context, imports, options, rootMembers, namespaceSyntax)),
130+
usings: SyntaxFactory.List(UsingsHelper.GetUsings(context, imports, options, namespaceNameSyntax)),
132131
attributeLists: [],
133132
members: SyntaxFactory.List(rootMembers)
134-
)
135-
.NormalizeWhitespace();
133+
);
136134

137135
root = root.WithPackageFileComments(context, result, package);
138136

JavaToCSharp/UsingsHelper.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using com.github.javaparser.ast;
2+
using Microsoft.CodeAnalysis;
23
using Microsoft.CodeAnalysis.CSharp;
34
using Microsoft.CodeAnalysis.CSharp.Syntax;
45

@@ -9,16 +10,15 @@ public static class UsingsHelper
910
public static IEnumerable<UsingDirectiveSyntax> GetUsings(ConversionContext context,
1011
IEnumerable<ImportDeclaration> imports,
1112
JavaConversionOptions? options,
12-
IEnumerable<MemberDeclarationSyntax> rootMembers,
13-
NamespaceDeclarationSyntax? namespaceSyntax)
13+
NameSyntax? namespaceNameSyntax)
1414
{
1515
var usings = new List<UsingDirectiveSyntax>();
1616

1717
foreach (var import in imports)
1818
{
1919
// The import directive in Java will import a specific class.
2020
string importName = import.getNameAsString();
21-
var lastPartStartIndex = importName.LastIndexOf(".", StringComparison.Ordinal);
21+
var lastPartStartIndex = importName.LastIndexOf('.');
2222
var importNameWithoutClassName = lastPartStartIndex == -1 ?
2323
importName :
2424
importName[..lastPartStartIndex];
@@ -30,29 +30,38 @@ public static IEnumerable<UsingDirectiveSyntax> GetUsings(ConversionContext cont
3030
usingSyntax = CommentsHelper.AddUsingComments(usingSyntax, import);
3131
}
3232

33-
usings.Add(usingSyntax);
33+
usings.Add(usingSyntax.NormalizeWhitespace().WithTrailingNewLines());
3434
}
3535

3636
if (options?.IncludeUsings == true)
3737
{
3838
usings.AddRange(options.Usings
3939
.Where(x => !string.IsNullOrWhiteSpace(x))
40-
.Select(ns => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns))));
40+
.Select(ns => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns)).NormalizeWhitespace().WithTrailingNewLines()));
4141
}
4242

43-
if (namespaceSyntax != null)
43+
if (namespaceNameSyntax != null)
4444
{
4545
foreach (var staticUsing in options?.StaticUsingEnumNames ?? [])
4646
{
4747
var usingSyntax = SyntaxFactory
48-
.UsingDirective(SyntaxFactory.ParseName($"{namespaceSyntax.Name}.{staticUsing}"))
49-
.WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword));
48+
.UsingDirective(SyntaxFactory.ParseName($"{namespaceNameSyntax}.{staticUsing}"))
49+
.WithStaticKeyword(SyntaxFactory.Token(SyntaxKind.StaticKeyword))
50+
.NormalizeWhitespace()
51+
.WithTrailingNewLines();
5052

5153
usings.Add(usingSyntax);
5254
}
5355
}
5456

55-
return usings.Distinct(new UsingDirectiveSyntaxComparer()).ToList();
57+
usings = usings.Distinct(new UsingDirectiveSyntaxComparer()).ToList();
58+
59+
if (usings.Count > 0)
60+
{
61+
usings[^1] = usings[^1].WithTrailingNewLines(2);
62+
}
63+
64+
return usings;
5665
}
5766
}
5867

JavaToCSharp/Whitespace.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
4+
namespace JavaToCSharp;
5+
6+
public static class Whitespace
7+
{
8+
public static SyntaxTrivia NewLine => Environment.NewLine == "\r\n"
9+
? SyntaxFactory.CarriageReturnLineFeed
10+
: SyntaxFactory.LineFeed;
11+
}

JavaToCSharpCli/Program.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ public class Program
4848
description: "Convert System.out calls to Console",
4949
getDefaultValue: () => false);
5050

51+
private static readonly Option<bool> _fileScopedNamespacesOption = new(
52+
name: "--file-scoped-namespaces",
53+
description: "Use file-scoped namespaces in C# output",
54+
getDefaultValue: () => false);
55+
5156
private static readonly Option<bool> _clearDefaultUsingsOption = new(
5257
name: "--clear-usings",
5358
description: "Remove all default usings provided by this app",
@@ -84,6 +89,7 @@ public static async Task Main(string[] args)
8489
rootCommand.AddGlobalOption(_startInterfaceNamesWithIOption);
8590
rootCommand.AddGlobalOption(_commentUnrecognizedCodeOption);
8691
rootCommand.AddGlobalOption(_systemOutToConsoleOption);
92+
rootCommand.AddGlobalOption(_fileScopedNamespacesOption);
8793
rootCommand.AddGlobalOption(_clearDefaultUsingsOption);
8894
rootCommand.AddGlobalOption(_addUsingsOption);
8995

@@ -131,7 +137,8 @@ private static JavaConversionOptions GetJavaConversionOptions(InvocationContext
131137
ConvertSystemOutToConsole = context.ParseResult.GetValueForOption(_systemOutToConsoleOption),
132138
StartInterfaceNamesWithI = context.ParseResult.GetValueForOption(_startInterfaceNamesWithIOption),
133139
UseDebugAssertForAsserts = context.ParseResult.GetValueForOption(_useDebugAssertOption),
134-
UseUnrecognizedCodeToComment = context.ParseResult.GetValueForOption(_commentUnrecognizedCodeOption)
140+
UseUnrecognizedCodeToComment = context.ParseResult.GetValueForOption(_commentUnrecognizedCodeOption),
141+
UseFileScopedNamespaces = context.ParseResult.GetValueForOption(_fileScopedNamespacesOption),
135142
};
136143

137144
if (context.ParseResult.GetValueForOption(_clearDefaultUsingsOption))

JavaToCSharpGui/App.config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<setting name="Usings" serializeAs="String">
2929
<value>System;System.Collections.Generic;System.Collections.ObjectModel;System.Linq;System.Text</value>
3030
</setting>
31+
<setting name="UseFileScopedNamespaces" serializeAs="String">
32+
<value>False</value>
33+
</setting>
3134
</JavaToCSharpGui.Properties.Settings>
3235
</userSettings>
3336
</configuration>

JavaToCSharpGui/CurrentOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ static CurrentOptions()
1313
Options.UseDebugAssertForAsserts = Settings.Default.UseDebugAssertPreference;
1414
Options.UseUnrecognizedCodeToComment = Settings.Default.UseUnrecognizedCodeToComment;
1515
Options.ConvertSystemOutToConsole = Settings.Default.ConvertSystemOutToConsole;
16+
Options.UseFileScopedNamespaces = Settings.Default.UseFileScopedNamespaces;
1617

1718
Options.SetUsings(Settings.Default.Usings.Split(';'));
1819
}
@@ -27,6 +28,7 @@ public static void Persist()
2728
Settings.Default.UseDebugAssertPreference = Options.UseDebugAssertForAsserts;
2829
Settings.Default.UseUnrecognizedCodeToComment = Options.UseUnrecognizedCodeToComment;
2930
Settings.Default.ConvertSystemOutToConsole = Options.ConvertSystemOutToConsole;
31+
Settings.Default.UseFileScopedNamespaces = Options.UseFileScopedNamespaces;
3032
Settings.Default.Usings = string.Join(";", Options.Usings);
3133

3234
Settings.Default.Save();

JavaToCSharpGui/Properties/Settings.Designer.cs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

JavaToCSharpGui/Properties/Settings.settings

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@
2323
<Setting Name="Usings" Type="System.String" Scope="User">
2424
<Value Profile="(Default)">System;System.Collections.Generic;System.Collections.ObjectModel;System.Linq;System.Text</Value>
2525
</Setting>
26+
<Setting Name="UseFileScopedNamespaces" Type="System.Boolean" Scope="User">
27+
<Value Profile="(Default)">False</Value>
28+
</Setting>
2629
</Settings>
2730
</SettingsFile>

JavaToCSharpGui/ViewModels/SettingsWindowViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public partial class SettingsWindowViewModel : ViewModelBase
2424

2525
[ObservableProperty] private bool _convertSystemOutToConsole = CurrentOptions.Options.ConvertSystemOutToConsole;
2626

27+
[ObservableProperty] private bool _useFileScopedNamespaces = CurrentOptions.Options.UseFileScopedNamespaces;
28+
2729
public event EventHandler? CloseRequested;
2830

2931
[RelayCommand]
@@ -51,6 +53,7 @@ private void Save()
5153
CurrentOptions.Options.UseDebugAssertForAsserts = UseDebugAssertForAsserts;
5254
CurrentOptions.Options.UseUnrecognizedCodeToComment = UnrecognizedCodeToComment;
5355
CurrentOptions.Options.ConvertSystemOutToConsole = ConvertSystemOutToConsole;
56+
CurrentOptions.Options.UseFileScopedNamespaces = UseFileScopedNamespaces;
5457

5558
CurrentOptions.Options.SetUsings(Usings);
5659

JavaToCSharpGui/Views/SettingsWindow.axaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
IsChecked="{CompiledBinding IncludeNamespace}">
3939
Include namespace in output
4040
</CheckBox>
41+
<CheckBox Margin="5" Name="UseFileScopedNamespaces"
42+
IsChecked="{CompiledBinding UseFileScopedNamespaces}">
43+
Use file-scoped namespaces
44+
</CheckBox>
4145
<CheckBox Margin="5" Name="IncludeComments"
4246
IsChecked="{CompiledBinding IncludeComments}">
4347
Include comments in output

0 commit comments

Comments
 (0)