diff --git a/CodeConverter/Common/LineTriviaMapper.cs b/CodeConverter/Common/LineTriviaMapper.cs index 46251dcf5..1effddf87 100644 --- a/CodeConverter/Common/LineTriviaMapper.cs +++ b/CodeConverter/Common/LineTriviaMapper.cs @@ -194,7 +194,10 @@ private TextLine GetTargetLine(int sourceLineIndex, bool isLeading) if (errorTrivia.Any()) { targetTrivia.Trailing.Add(errorTrivia); } - errorTrivia = toReplace.LeadingTrivia.Where(t => t.HasAnnotations(AnnotationConstants.ConversionErrorAnnotationKind)).ToArray(); + // A leading error comment keeps the line break after it, or it would comment out the token it precedes + var leading = toReplace.LeadingTrivia; + errorTrivia = leading.Where((t, i) => t.HasAnnotations(AnnotationConstants.ConversionErrorAnnotationKind) || + t.IsEndOfLine() && i > 0 && leading[i - 1].HasAnnotations(AnnotationConstants.ConversionErrorAnnotationKind)).ToArray(); if (errorTrivia.Any()) { targetTrivia.Leading.Add(errorTrivia); } diff --git a/CodeConverter/Util/SyntaxNodeExtensions.cs b/CodeConverter/Util/SyntaxNodeExtensions.cs index f5f74ea62..0c7c9813e 100644 --- a/CodeConverter/Util/SyntaxNodeExtensions.cs +++ b/CodeConverter/Util/SyntaxNodeExtensions.cs @@ -540,7 +540,9 @@ public static T WithVbTrailingErrorComment( this T dummyDestNode, CSharpSyntaxNode problematicSourceNode, Exception exception) where T : VisualBasicSyntaxNode { var errorDescription = problematicSourceNode.DescribeConversionError(exception); - var commentedText = "''' " + errorDescription.Replace("\r\n", "\r\n''' "); + // Split on either line ending, since the description mixes Environment.NewLine with source text and stack traces + var lines = errorDescription.TrimEnd('\r', '\n').Split(new[] {"\r\n", "\n"}, StringSplitOptions.None); + var commentedText = string.Join(Environment.NewLine, lines.Select(line => "''' " + line)); return dummyDestNode .WithTrailingTrivia(VBSyntaxFactory.CommentTrivia(commentedText).WithConversionErrorAnnotation()) .WithAdditionalAnnotations(new SyntaxAnnotation(AnnotationConstants.ConversionErrorAnnotationKind, exception.ToString())); diff --git a/CodeConverter/VB/CommonConversions.cs b/CodeConverter/VB/CommonConversions.cs index a150aea91..d07ac9098 100644 --- a/CodeConverter/VB/CommonConversions.cs +++ b/CodeConverter/VB/CommonConversions.cs @@ -157,7 +157,12 @@ private VariableDeclaratorSyntax ConvertToVariableDeclarator(CSSyntax.Declaratio private VariableDeclaratorSyntax ConvertToVariableDeclaratorOrNull(CSSyntax.IsPatternExpressionSyntax node) { - switch (node.Pattern) { + return ConvertPatternToVariableDeclaratorOrNull(node.Pattern); + } + + private VariableDeclaratorSyntax ConvertPatternToVariableDeclaratorOrNull(CSSyntax.PatternSyntax pattern) + { + switch (pattern) { case CSSyntax.DeclarationPatternSyntax d: { var id = ((IdentifierNameSyntax)d.Designation.Accept(_nodesVisitor)).Identifier; var ids = SyntaxFactory.SingletonSeparatedList(SyntaxFactory.ModifiedIdentifier(id)); @@ -169,10 +174,12 @@ private VariableDeclaratorSyntax ConvertToVariableDeclaratorOrNull(CSSyntax.IsPa SyntaxFactory.Token(SyntaxKind.NothingKeyword))); return SyntaxFactory.VariableDeclarator(ids, simpleAsClauseSyntax, equalsValueSyntax); } - case CSSyntax.ConstantPatternSyntax _: - return null; + case CSSyntax.UnaryPatternSyntax u: + // "x is not T v" declares v just the same, so the negated pattern is hoisted too + return ConvertPatternToVariableDeclaratorOrNull(u.Pattern); default: - throw new ArgumentOutOfRangeException(nameof(node), node.Pattern, null); + // no other pattern introduces a variable, and the caller already filters nulls out + return null; } } diff --git a/CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs index 58c9cabd1..1505b2ed0 100644 --- a/CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs @@ -572,11 +572,23 @@ public override SyntaxList VisitReturnStatement(CSSyntax.Return private static StatementSyntax ReturnStatement(VisualBasicSyntaxNode vbExpression) { - return vbExpression == null - ? SyntaxFactory.ReturnStatement() - : vbExpression.IsKind(SyntaxKind.EmptyStatement) - ? SyntaxFactory.ReturnStatement() - : SyntaxFactory.ReturnStatement((ExpressionSyntax)vbExpression); + if (vbExpression == null) { + return SyntaxFactory.ReturnStatement(); + } + + // A failed conversion comes back as an empty statement carrying the report as trivia. The + // expression cannot be returned, but the report has to travel with the statement or the + // return silently loses its value and nothing says so. It goes ahead of the statement + // because a comment runs to the end of its line and would otherwise swallow the Return. + if (vbExpression.IsKind(SyntaxKind.EmptyStatement)) { + var report = vbExpression.GetLeadingTrivia() + .AddRange(vbExpression.GetTrailingTrivia()) + .Add(SyntaxFactory.EndOfLine(Environment.NewLine)); + + return SyntaxFactory.ReturnStatement().WithLeadingTrivia(report); + } + + return SyntaxFactory.ReturnStatement((ExpressionSyntax)vbExpression); } public override SyntaxList VisitYieldStatement(CSSyntax.YieldStatementSyntax node) diff --git a/CodeConverter/VB/NodesVisitor.cs b/CodeConverter/VB/NodesVisitor.cs index 979c58d2c..828f17446 100644 --- a/CodeConverter/VB/NodesVisitor.cs +++ b/CodeConverter/VB/NodesVisitor.cs @@ -11,6 +11,7 @@ using ArrayRankSpecifierSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.ArrayRankSpecifierSyntax; using AttributeListSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeListSyntax; using AttributeSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeSyntax; +using BinaryExpressionSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.BinaryExpressionSyntax; using ExpressionSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.ExpressionSyntax; using IdentifierNameSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.IdentifierNameSyntax; using InterpolatedStringContentSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.InterpolatedStringContentSyntax; @@ -30,6 +31,7 @@ using SyntaxKind = Microsoft.CodeAnalysis.VisualBasic.SyntaxKind; using TupleElementSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.TupleElementSyntax; using TypeArgumentListSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.TypeArgumentListSyntax; +using TypeOfExpressionSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.TypeOfExpressionSyntax; using TypeParameterConstraintClauseSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.TypeParameterConstraintClauseSyntax; using TypeParameterListSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.TypeParameterListSyntax; using TypeParameterSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.TypeParameterSyntax; @@ -348,7 +350,18 @@ public override VisualBasicSyntaxNode VisitConstructorDeclaration(CSSyntax.Const public override VisualBasicSyntaxNode VisitIsPatternExpression(CSSyntax.IsPatternExpressionSyntax node) { ExpressionSyntax lhs = (ExpressionSyntax)node.Expression.Accept(TriviaConvertingVisitor); - switch (node.Pattern) { + var converted = ConvertPattern(node, lhs, _semanticModel.GetTypeInfo(node.Expression).Type, node.Pattern); + // In C# "is" binds tighter than ==, but VB's comparison operators (including Is) share one precedence level + return node.Parent is CSSyntax.BinaryExpressionSyntax ? SyntaxFactory.ParenthesizedExpression(converted) : converted; + } + + /// + /// Separate from the visitor so that a negated pattern can convert the pattern it wraps. + /// The type of the value under test travels along because picking the right comparison depends on it. + /// + private ExpressionSyntax ConvertPattern(CSSyntax.IsPatternExpressionSyntax node, ExpressionSyntax lhs, ITypeSymbol lhsType, CSSyntax.PatternSyntax pattern) + { + switch (pattern) { case CSSyntax.DeclarationPatternSyntax d: { var left = (ExpressionSyntax)d.Designation.Accept(TriviaConvertingVisitor); ExpressionSyntax right = SyntaxFactory.TryCastExpression( @@ -360,11 +373,99 @@ public override VisualBasicSyntaxNode VisitIsPatternExpression(CSSyntax.IsPatter SyntaxFactory.Token(SyntaxKind.NothingKeyword)); return SyntaxFactory.IsNotExpression(tryCast, nothingExpression); } - case CSSyntax.ConstantPatternSyntax cps: - return SyntaxFactory.IsExpression(lhs, - (ExpressionSyntax)cps.Expression.Accept(TriviaConvertingVisitor)); + case CSSyntax.TypePatternSyntax tp: + return SyntaxFactory.TypeOfIsExpression(lhs, (TypeSyntax)tp.Type.Accept(TriviaConvertingVisitor)); + case CSSyntax.UnaryPatternSyntax u when u.OperatorToken.IsKind(CS.SyntaxKind.NotKeyword): + return NegatePattern(ConvertPattern(node, lhs, lhsType, u.Pattern)); + case CSSyntax.RelationalPatternSyntax rel: + return ConvertRelationalPattern(lhs, lhsType, rel); + case CSSyntax.ConstantPatternSyntax cps: { + // an identifier bound to a type is a type test, not a comparison against a value + if (_semanticModel.GetSymbolInfo(cps.Expression).ExtractBestMatch() is { } typeSymbol) { + return SyntaxFactory.TypeOfIsExpression(lhs, + (TypeSyntax)_commonConversions.VbSyntaxGenerator.TypeExpression(typeSymbol)); + } + + var constant = (ExpressionSyntax)cps.Expression.Accept(TriviaConvertingVisitor); + + if (cps.Expression.IsKind(CS.SyntaxKind.NullLiteralExpression)) { + return SyntaxFactory.IsExpression(lhs, constant); + } + + // On a nullable value type, = lifts to Boolean? (which throws when narrowed from Nothing), and + // Equals compares the boxed types, so a Long? holding 5 is never Equals to the Integer 5 + if (lhsType.IsNullable()) { + throw new NotSupportedException("A constant pattern on a nullable value type is not converted."); + } + + // NaN matches NaN in a pattern, but = never does + if (lhsType != null && lhsType.IsValueType && _semanticModel.GetConstantValue(cps.Expression).Value is double.NaN or float.NaN) { + var isNaN = SyntaxFactory.SimpleMemberAccessExpression( + (ExpressionSyntax)_commonConversions.VbSyntaxGenerator.TypeExpression(lhsType), + SyntaxFactory.Token(SyntaxKind.DotToken), SyntaxFactory.IdentifierName(nameof(double.IsNaN))); + return SyntaxFactory.InvocationExpression(isNaN, ExpressionSyntaxExtensions.CreateArgList(lhs)); + } + + // A constant pattern compares by value. VB's = does that on a value type, but on an + // Object it goes through late binding and throws where C# would answer False, so + // anything not known to be a value type is compared through Equals instead. + if (lhsType != null && lhsType.IsValueType) { + return SyntaxFactory.EqualsExpression(lhs, constant); + } + + return SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName(nameof(Equals)), + ExpressionSyntaxExtensions.CreateArgList(lhs, constant)); + } default: - throw new ArgumentOutOfRangeException(nameof(node), node.Pattern, null); + throw new ArgumentOutOfRangeException(nameof(node), pattern, null); + } + } + + /// + /// "x is > 0" and its siblings are plain comparisons once the value under test is known. + /// On anything but a non-nullable value type, the pattern also tests the type or that a value is + /// present, which a plain comparison would silently drop (late binding, or a lifted Boolean?). + /// + private ExpressionSyntax ConvertRelationalPattern(ExpressionSyntax lhs, ITypeSymbol lhsType, CSSyntax.RelationalPatternSyntax pattern) + { + if (lhsType == null || !lhsType.IsValueType || lhsType.IsNullable()) { + throw new NotSupportedException("A relational pattern is only converted when the value under test is a non-nullable value type."); + } + + var value = (ExpressionSyntax)pattern.Expression.Accept(TriviaConvertingVisitor); + var op = pattern.OperatorToken; + + if (op.IsKind(CS.SyntaxKind.GreaterThanToken)) { + return SyntaxFactory.GreaterThanExpression(lhs, value); + } + if (op.IsKind(CS.SyntaxKind.GreaterThanEqualsToken)) { + return SyntaxFactory.GreaterThanOrEqualExpression(lhs, value); + } + if (op.IsKind(CS.SyntaxKind.LessThanToken)) { + return SyntaxFactory.LessThanExpression(lhs, value); + } + if (op.IsKind(CS.SyntaxKind.LessThanEqualsToken)) { + return SyntaxFactory.LessThanOrEqualExpression(lhs, value); + } + + throw new ArgumentOutOfRangeException(nameof(pattern), op.Text, null); + } + + /// + /// Visual Basic can negate a type test and a Nothing test in place, which reads far better + /// than wrapping the whole condition in Not. + /// + private static ExpressionSyntax NegatePattern(ExpressionSyntax converted) + { + switch (converted) { + case TypeOfExpressionSyntax typeOf when typeOf.IsKind(SyntaxKind.TypeOfIsExpression): + return SyntaxFactory.TypeOfIsNotExpression(typeOf.Expression, typeOf.Type); + case BinaryExpressionSyntax binary when binary.IsKind(SyntaxKind.IsNotExpression): + return SyntaxFactory.IsExpression(binary.Left, binary.Right); + case BinaryExpressionSyntax binary when binary.IsKind(SyntaxKind.IsExpression): + return SyntaxFactory.IsNotExpression(binary.Left, binary.Right); + default: + return SyntaxFactory.NotExpression(SyntaxFactory.ParenthesizedExpression(converted)); } } @@ -991,7 +1092,7 @@ public override VisualBasicSyntaxNode VisitAssignmentExpression(CSSyntax.Assignm var right = (ExpressionSyntax)node.Right.Accept(TriviaConvertingVisitor); if (IsReturnValueDiscarded(node)) { if (_semanticModel.GetTypeInfo(node.Right).ConvertedType.IsDelegateType()) { - if (_semanticModel.GetSymbolInfo(node.Left).Symbol.Kind != SymbolKind.Event) { + if (_semanticModel.GetSymbolInfo(node.Left).Symbol?.Kind != SymbolKind.Event) { var kind = node.GetAncestor()?.Kind(); if (kind != null && (kind.Value == CS.SyntaxKind.AddAccessorDeclaration || kind.Value == CS.SyntaxKind.RemoveAccessorDeclaration)) { var methodName = kind.Value == CS.SyntaxKind.AddAccessorDeclaration ? "Combine" : "Remove"; @@ -1391,6 +1492,27 @@ public override VisualBasicSyntaxNode VisitObjectCreationExpression(CSSyntax.Obj ); } + /// + /// "new(...)" takes its type from what it is assigned to, which Visual Basic cannot do, so the + /// type the compiler settled on is written out. + /// + public override VisualBasicSyntaxNode VisitImplicitObjectCreationExpression(CSSyntax.ImplicitObjectCreationExpressionSyntax node) + { + var typeInfo = _semanticModel.GetTypeInfo(node); + var typeSymbol = typeInfo.Type ?? typeInfo.ConvertedType; + + if (typeSymbol == null) { + throw new NotSupportedException("The type of an implicit object creation could not be resolved."); + } + + return SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.List(), + (TypeSyntax)_commonConversions.VbSyntaxGenerator.TypeExpression(typeSymbol), + (ArgumentListSyntax)node.ArgumentList?.Accept(TriviaConvertingVisitor), + (ObjectCreationInitializerSyntax)node.Initializer?.Accept(TriviaConvertingVisitor) + ); + } + public override VisualBasicSyntaxNode VisitAnonymousObjectCreationExpression(CSSyntax.AnonymousObjectCreationExpressionSyntax node) { return SyntaxFactory.AnonymousObjectCreationExpression( diff --git a/Tests/VB/ExpressionTests.cs b/Tests/VB/ExpressionTests.cs index fd10e5672..4e2e5f431 100644 --- a/Tests/VB/ExpressionTests.cs +++ b/Tests/VB/ExpressionTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using ICSharpCode.CodeConverter.Tests.TestRunners; +using ICSharpCode.CodeConverter.VB; using Xunit; namespace ICSharpCode.CodeConverter.Tests.VB; @@ -253,6 +254,302 @@ End Class BC30451: 'CSharpImpl.__Assign' is not declared. It may be inaccessible due to its protection level."); } + [Fact] + public async Task NegatedTypePatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool IsNotString(object node) + { + return node is not string; + } +}", @"Friend Class TestClass + Private Shared Function IsNotString(node As Object) As Boolean + Return TypeOf node IsNot String + End Function +End Class"); + } + + [Fact] + public async Task NegatedNullPatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool IsNotNull(object node) + { + return node is not null; + } +}", @"Friend Class TestClass + Private Shared Function IsNotNull(node As Object) As Boolean + Return node IsNot Nothing + End Function +End Class"); + } + + [Fact] + public async Task NegatedNamedTypePatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class Picture +{ +} + +class TestClass +{ + private static bool IsNotPicture(object node) + { + return node is not Picture; + } +}", @"Friend Class Picture +End Class + +Friend Class TestClass + Private Shared Function IsNotPicture(node As Object) As Boolean + Return TypeOf node IsNot Picture + End Function +End Class"); + } + + [Fact] + public async Task NegatedDeclarationPatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static int GetLength(object node) + { + if (node is not string s) + { + return -1; + } + + return s.Length; + } +}", @"Friend Class TestClass + Private Shared Function GetLength(node As Object) As Integer + Dim s As String = Nothing + + If CSharpImpl.__Assign(s, TryCast(node, String)) Is Nothing Then + Return -1 + End If + + Return s.Length + End Function + + Private Class CSharpImpl + + Shared Function __Assign(Of T)(ByRef target As T, value As T) As T + target = value + Return value + End Function + End Class +End Class + +1 target compilation errors: +BC30451: 'CSharpImpl.__Assign' is not declared. It may be inaccessible due to its protection level."); + } + + [Fact] + public async Task RelationalPatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool IsPositive(int i) + { + return i is > 0; + } +}", @"Friend Class TestClass + Private Shared Function IsPositive(i As Integer) As Boolean + Return i > 0 + End Function +End Class"); + } + + [Fact] + public async Task RelationalPatternOnNonValueTypeIsNotConvertedAsync() + { + // A plain comparison would drop the type test (late binding on Object) or the HasValue test (lifted Boolean?) + var convertedCode = await ConvertAsync(@"class TestClass +{ + private static void IsPositive(object o, int? i) + { + bool objectPositive = o is > 0; + bool nullablePositive = i is > 0; + } +}"); + + Assert.Contains("o is > 0", convertedCode); + Assert.Contains("i is > 0", convertedCode); + Assert.DoesNotContain("o > 0", convertedCode); + Assert.DoesNotContain("i > 0", convertedCode); + } + + [Fact] + public async Task ConstantPatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool ValueTypeConstant(int i) + { + return i is 5; + } + + private static bool ObjectConstant(object o) + { + return o is 5; + } + + private static bool StringConstant(string s) + { + return s is ""abc""; + } +}", @"Friend Class TestClass + Private Shared Function ValueTypeConstant(i As Integer) As Boolean + Return i = 5 + End Function + + Private Shared Function ObjectConstant(o As Object) As Boolean + Return Equals(o, 5) + End Function + + Private Shared Function StringConstant(s As String) As Boolean + Return Equals(s, ""abc"") + End Function +End Class"); + } + + [Fact] + public async Task PatternInsideComparisonKeepsPrecedenceAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool ConstantPattern(bool flag, int x) + { + return flag == x is 5; + } + + private static bool RelationalPattern(bool flag, int x) + { + return flag != x is > 0; + } + + private static bool NullPattern(bool flag, object o) + { + return flag == o is null; + } + + private static bool LogicalOperator(bool flag, object o) + { + return flag && o is not null; + } +}", @"Friend Class TestClass + Private Shared Function ConstantPattern(flag As Boolean, x As Integer) As Boolean + Return flag = (x = 5) + End Function + + Private Shared Function RelationalPattern(flag As Boolean, x As Integer) As Boolean + Return flag <> (x > 0) + End Function + + Private Shared Function NullPattern(flag As Boolean, o As Object) As Boolean + Return flag = (o Is Nothing) + End Function + + Private Shared Function LogicalOperator(flag As Boolean, o As Object) As Boolean + Return flag AndAlso o IsNot Nothing + End Function +End Class"); + } + + [Fact] + public async Task NullableConstantPatternIsNotConvertedAsync() + { + // "=" lifts to Boolean? (throws when narrowed from Nothing), and Equals compares boxed types (a Long? is never Equals to an Integer constant) + var convertedCode = await ConvertAsync(@"class TestClass +{ + private static void NullableConstant(long? l) + { + bool isFive = l is 5; + } +}"); + + Assert.Contains("l is 5", convertedCode); + Assert.DoesNotContain("l = 5", convertedCode); + Assert.DoesNotContain("Equals(l, 5)", convertedCode); + } + + [Fact] + public async Task NaNConstantPatternExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool DoubleIsNaN(double d) + { + return d is double.NaN; + } + + private static bool SingleIsNaN(float f) + { + return f is not float.NaN; + } + + private static bool ObjectIsNaN(object o) + { + return o is double.NaN; + } +}", @"Friend Class TestClass + Private Shared Function DoubleIsNaN(d As Double) As Boolean + Return Double.IsNaN(d) + End Function + + Private Shared Function SingleIsNaN(f As Single) As Boolean + Return Not Single.IsNaN(f) + End Function + + Private Shared Function ObjectIsNaN(o As Object) As Boolean + Return Equals(o, Double.NaN) + End Function +End Class"); + } + + [Fact] + public async Task NullablePatternAgainstNullAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"class TestClass +{ + private static bool IsNull(int? i) + { + return i is null; + } + + private static bool IsNotNull(int? i) + { + return i is not null; + } +}", @"Friend Class TestClass + Private Shared Function IsNull(i As Integer?) As Boolean + Return i Is Nothing + End Function + + Private Shared Function IsNotNull(i As Integer?) As Boolean + Return i IsNot Nothing + End Function +End Class"); + } + + [Fact] + public async Task ImplicitObjectCreationExpressionAsync() + { + await TestConversionCSharpToVisualBasicAsync(@"using System.Text; + +class TestClass +{ + private StringBuilder builder = new(16); +}", @"Imports System.Text + +Friend Class TestClass + Private builder As StringBuilder = New StringBuilder(16) +End Class"); + } + [Fact] public async Task DeclarationExpressionAsync() { diff --git a/Tests/VB/StatementTests.cs b/Tests/VB/StatementTests.cs index 13eb44313..2bd9eb585 100644 --- a/Tests/VB/StatementTests.cs +++ b/Tests/VB/StatementTests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; using ICSharpCode.CodeConverter.Tests.TestRunners; using ICSharpCode.CodeConverter.VB; using Xunit; @@ -605,6 +606,33 @@ void TestMethod() Assert.Contains("End If", convertedCode); } + [Fact] + public async Task ReturnOfUnconvertibleExpressionKeepsConversionErrorAsync() + { + // Property patterns aren't supported, so the returned expression fails to convert + var convertedCode = await ConvertAsync(@"class Person +{ + public string Name { get; set; } +} + +class TestClass +{ + private static bool IsX(Person p) + { + // leading comment + return p is { Name: ""x"" }; // trailing comment + } +}"); + + var lines = convertedCode.Split('\n').Select(l => l.Trim()).ToList(); + var errorStart = lines.FindIndex(l => l.StartsWith("''' Cannot convert IsPatternExpressionSyntax")); + var returnLine = lines.IndexOf("Return ' trailing comment"); + Assert.True(errorStart >= 0 && returnLine > errorStart, convertedCode); + Assert.All(lines.Skip(errorStart).Take(returnLine - errorStart), l => Assert.StartsWith("'''", l)); + Assert.Single(lines, l => l.Contains("leading comment")); + Assert.Single(lines, l => l.Contains("trailing comment")); + } + [Fact] public async Task DoWhileStatementAsync() {