diff --git a/JavaToCSharp.Tests/EscapeIdentifierTests.cs b/JavaToCSharp.Tests/EscapeIdentifierTests.cs index 60a80c4..324ea27 100644 --- a/JavaToCSharp.Tests/EscapeIdentifierTests.cs +++ b/JavaToCSharp.Tests/EscapeIdentifierTests.cs @@ -141,6 +141,160 @@ public void bar(List items) { Assert.Contains("(@object) => Print(@object)", parsed); } + /// + /// Regression test for #133: the declaration of a local variable named after a C# keyword + /// was left unescaped even though its usages were escaped. + /// + [Fact] + public void ConvertText_GivenLocalVariableNamedAfterKeyword_ShouldEscapeDeclaration() + { + const string javaCode = """ + public class Foo { + public void bar() { + InputStream in = openStream(); + init(in); + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("InputStream @in = OpenStream();", parsed); + Assert.Contains("Init(@in);", parsed); + } + + [Fact] + public void ConvertText_GivenTryWithResourcesNamedAfterKeyword_ShouldEscapeDeclaration() + { + const string javaCode = """ + public class Foo { + public void bar(File dictionaryFile) { + try (InputStream in = new FileInputStream(dictionaryFile)) { + init(in); + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("using (InputStream @in = new FileInputStream(dictionaryFile))", parsed); + Assert.Contains("Init(@in);", parsed); + } + + [Fact] + public void ConvertText_GivenFieldNamedAfterKeyword_ShouldEscapeDeclaration() + { + const string javaCode = """ + public class Foo { + private int base = 1; + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("@base", parsed); + } + + [Fact] + public void ConvertText_GivenForEachVariableNamedAfterKeyword_ShouldEscapeDeclaration() + { + const string javaCode = """ + public class Foo { + public void bar(List items) { + for (String object : items) { + print(object); + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("foreach (string @object in items)", parsed); + Assert.Contains("Print(@object);", parsed); + } + + [Fact] + public void ConvertText_GivenForLoopVariableNamedAfterKeyword_ShouldEscapeDeclaration() + { + const string javaCode = """ + public class Foo { + public void bar() { + for (int base = 0; base < 10; base++) { + print(base); + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("for (int @base = 0; @base < 10; @base++)", parsed); + Assert.Contains("Print(@base);", parsed); + } + + [Fact] + public void ConvertText_GivenForLoopWithMultipleVariables_ShouldEscapeOnlyKeywords() + { + const string javaCode = """ + public class Foo { + public void bar() { + for (int base = 0, i = 1; base < 10; base++) { + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("int @base = 0, i = 1;", parsed); + } + + [Fact] + public void ConvertText_GivenCatchParameterNamedAfterKeyword_ShouldEscapeParameter() + { + const string javaCode = """ + public class Foo { + public void bar() { + try { + baz(); + } catch (Exception event) { + handle(event); + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("catch (Exception @event)", parsed); + Assert.Contains("Handle(@event);", parsed); + } + + /// + /// Java 9+ allows an already-declared effectively-final variable as a try-with-resources + /// resource. That is a reference, not a declaration, but it still needs escaping. + /// + [Fact] + public void ConvertText_GivenTryWithResourcesNameExpressionNamedAfterKeyword_ShouldEscapeReference() + { + const string javaCode = """ + public class Foo { + public void bar() throws Exception { + InputStream in = openStream(); + try (in) { + init(in); + } + } + } + """; + + var parsed = Convert(javaCode); + + Assert.Contains("using (@in)", parsed); + } + /// /// Escaped identifiers must survive a round trip through the C# parser without producing /// diagnostics, which is what the original crash was really about. diff --git a/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs b/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs index d37cc9e..875cdf0 100644 --- a/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs +++ b/JavaToCSharp/Declarations/FieldDeclarationVisitor.cs @@ -53,12 +53,12 @@ public override MemberDeclarationSyntax VisitForClass( if (initSyntax is not null) { - var varDeclarationSyntax = SyntaxFactory.VariableDeclarator(name).WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)); + var varDeclarationSyntax = SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(name)).WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)); variables.Add(varDeclarationSyntax); } } else - variables.Add(SyntaxFactory.VariableDeclarator(name)); + variables.Add(SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(name))); } var typeSyntax = TypeHelper.ConvertTypeSyntax(commonType, arrayRank ?? 0); diff --git a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs index 262cdbc..12635f6 100644 --- a/JavaToCSharp/Statements/ExpressionStatementVisitor.cs +++ b/JavaToCSharp/Statements/ExpressionStatementVisitor.cs @@ -62,13 +62,13 @@ private static StatementSyntax VisitVariableDeclarationStatement(ConversionConte var initSyntax = ExpressionVisitor.VisitExpression(context, initExpr); if (initSyntax is not null) { - var varDeclarationSyntax = SyntaxFactory.VariableDeclarator(name).WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)); + var varDeclarationSyntax = SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(name)).WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)); variables.Add(varDeclarationSyntax); } } else { - variables.Add(SyntaxFactory.VariableDeclarator(name)); + variables.Add(SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(name))); } } diff --git a/JavaToCSharp/Statements/ForEachStatementVisitor.cs b/JavaToCSharp/Statements/ForEachStatementVisitor.cs index 19a43be..fa6f16d 100644 --- a/JavaToCSharp/Statements/ForEachStatementVisitor.cs +++ b/JavaToCSharp/Statements/ForEachStatementVisitor.cs @@ -22,13 +22,11 @@ public class ForEachStatementVisitor : StatementVisitor var type = TypeHelper.ConvertType(varType); var variableDeclarators = varExpr.getVariables()?.ToList()?? []; - var vars = variableDeclarators - .Select(i => SyntaxFactory.VariableDeclarator(i.toString())) - .ToArray(); + var identifier = SyntaxFactory.ParseToken(TypeHelper.EscapeIdentifier(variableDeclarators[0].getNameAsString())); var body = foreachStmt.getBody(); var bodySyntax = VisitStatement(context, body); - return bodySyntax is null ? null : SyntaxFactory.ForEachStatement(SyntaxFactory.ParseTypeName(type), vars[0].Identifier.ValueText, iterableSyntax, bodySyntax); + return bodySyntax is null ? null : SyntaxFactory.ForEachStatement(SyntaxFactory.ParseTypeName(type), identifier, iterableSyntax, bodySyntax); } } diff --git a/JavaToCSharp/Statements/ForStatementVisitor.cs b/JavaToCSharp/Statements/ForStatementVisitor.cs index 1f20c53..cac4ae3 100644 --- a/JavaToCSharp/Statements/ForStatementVisitor.cs +++ b/JavaToCSharp/Statements/ForStatementVisitor.cs @@ -26,7 +26,22 @@ public class ForStatementVisitor : StatementVisitor var variableDeclarators = varExpr.getVariables()?.ToList() ?? []; var vars = variableDeclarators - .Select(i => SyntaxFactory.VariableDeclarator(i.toString())) + .Select(i => + { + var declarator = SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(i.getNameAsString())); + var varInit = i.getInitializer().FromOptional(); + + if (varInit is null) + { + return declarator; + } + + var varInitSyntax = ExpressionVisitor.VisitExpression(context, varInit); + + return varInitSyntax is null + ? declarator + : declarator.WithInitializer(SyntaxFactory.EqualsValueClause(varInitSyntax)); + }) .ToArray(); varSyntax = SyntaxFactory.VariableDeclaration(SyntaxFactory.ParseTypeName(type), SyntaxFactory.SeparatedList(vars, Enumerable.Repeat(SyntaxFactory.Token(SyntaxKind.CommaToken), vars.Length - 1))); diff --git a/JavaToCSharp/Statements/TryStatementVisitor.cs b/JavaToCSharp/Statements/TryStatementVisitor.cs index e842134..02bc352 100644 --- a/JavaToCSharp/Statements/TryStatementVisitor.cs +++ b/JavaToCSharp/Statements/TryStatementVisitor.cs @@ -40,7 +40,7 @@ public override StatementSyntax Visit(ConversionContext context, TryStmt tryStmt { result = SyntaxFactory.UsingStatement(result) .WithExpression( - SyntaxFactory.IdentifierName(resource.asNameExpr().getNameAsString())); + SyntaxFactory.IdentifierName(TypeHelper.EscapeIdentifier(resource.asNameExpr().getNameAsString()))); } else if (resource.isVariableDeclarationExpr()) { @@ -63,7 +63,7 @@ public override StatementSyntax Visit(ConversionContext context, TryStmt tryStmt SyntaxFactory.VariableDeclaration( SyntaxFactory.ParseTypeName(TypeHelper.ConvertType(variable.getType())), SyntaxFactory.SeparatedList([ - SyntaxFactory.VariableDeclarator(variable.getNameAsString()) + SyntaxFactory.VariableDeclarator(TypeHelper.EscapeIdentifier(variable.getNameAsString())) .WithInitializer(SyntaxFactory.EqualsValueClause(initSyntax)) ]) )); @@ -143,7 +143,7 @@ private static TryStatementSyntax AddCatches(ConversionContext context, CatchCla SyntaxFactory.CatchClause( SyntaxFactory.CatchDeclaration( SyntaxFactory.ParseTypeName(type), - SyntaxFactory.ParseToken(ctch.getParameter().getNameAsString()) + SyntaxFactory.ParseToken(TypeHelper.EscapeIdentifier(ctch.getParameter().getNameAsString())) ), filter: null, block: catchBlockSyntax