From c43e145da241c1b1615ecf4adce196b7d2eae203 Mon Sep 17 00:00:00 2001 From: Travis Jones <31292511+CosmonautJones@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:43:17 -0400 Subject: [PATCH 1/3] Fix VB XML attribute-axis read conversion --- CodeConverter/CSharp/ExpressionNodeVisitor.cs | 26 +- .../MethodBodyExecutableStatementVisitor.cs | 3 +- .../CSharp/NameExpressionNodeVisitor.cs | 38 +-- .../CSharp/XmlExpressionConverter.cs | 93 ++++++-- .../ExpressionTests/XmlExpressionTests.cs | 12 + .../VBToCS/XmlAttributeAxisTests.vb | 224 ++++++++++++++++++ 6 files changed, 361 insertions(+), 35 deletions(-) create mode 100644 Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb diff --git a/CodeConverter/CSharp/ExpressionNodeVisitor.cs b/CodeConverter/CSharp/ExpressionNodeVisitor.cs index c41ef5576..42960129a 100644 --- a/CodeConverter/CSharp/ExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/ExpressionNodeVisitor.cs @@ -20,6 +20,7 @@ internal partial class ExpressionNodeVisitor : VBasic.VisualBasicSyntaxVisitor _extraUsingDirectives; private readonly IOperatorConverter _operatorConverter; private readonly Stack _withBlockLhs = new(); + private readonly Dictionary _conditionalAccessReceivers = new(); private readonly ITypeContext _typeContext; private readonly QueryConverter _queryConverter; private readonly LambdaConverter _lambdaConverter; @@ -44,8 +45,8 @@ public ExpressionNodeVisitor(SemanticModel semanticModel, _typeContext = typeContext; _extraUsingDirectives = extraUsingDirectives; _argumentConverter = new ArgumentConverter(visualBasicEqualityComparison, typeContext, semanticModel, commonConversions); - _xmlExpressionConverter = new XmlExpressionConverter(xmlImportContext, extraUsingDirectives, TriviaConvertingExpressionVisitor); - _nameExpressionNodeVisitor = new NameExpressionNodeVisitor(semanticModel, _generatedNames, typeContext, extraUsingDirectives, _tempNameForAnonymousScope, _withBlockLhs, commonConversions, _argumentConverter, TriviaConvertingExpressionVisitor); + _xmlExpressionConverter = new XmlExpressionConverter(xmlImportContext, extraUsingDirectives, commonConversions, semanticModel, _withBlockLhs, _generatedNames, _conditionalAccessReceivers); + _nameExpressionNodeVisitor = new NameExpressionNodeVisitor(semanticModel, _generatedNames, typeContext, extraUsingDirectives, _tempNameForAnonymousScope, _withBlockLhs, commonConversions, _argumentConverter, TriviaConvertingExpressionVisitor, _conditionalAccessReceivers); _operatorConverter = VbOperatorConversion.Create(TriviaConvertingExpressionVisitor, semanticModel, visualBasicEqualityComparison, commonConversions.TypeConversionAnalyzer); _binaryExpressionConverter = new BinaryExpressionConverter(semanticModel, _operatorConverter, visualBasicEqualityComparison, visualBasicNullableTypesConverter, commonConversions); } @@ -212,6 +213,27 @@ public override async Task VisitParenthesizedExpression(VBasic public override async Task VisitConditionalAccessExpression(VBasic.Syntax.ConditionalAccessExpressionSyntax node) { var leftExpression = await node.Expression.AcceptAsync(TriviaConvertingExpressionVisitor) ?? _withBlockLhs.Peek(); + if (node.WhenNotNull.DescendantNodesAndSelf().OfType() + .Any(axis => axis.Token2.Text == "@" && axis.Ancestors().TakeWhile(NameExpressionNodeVisitor.IsMemberAccessChain).Contains(node))) { + // Attribute reads need a cast or a sequence null guard, neither of which can wrap a member binding. + var receiver = ValidSyntaxFactory.IdentifierName(NameGenerator.CS.GetUniqueVariableNameInScope(_semanticModel, _generatedNames, node, "xmlReceiver")); + _conditionalAccessReceivers.Add(node, receiver); + try { + var whenNotNull = await node.WhenNotNull.AcceptAsync(TriviaConvertingExpressionVisitor); + var condition = CS.SyntaxFactory.IsPatternExpression(leftExpression, + CS.SyntaxFactory.RecursivePattern().WithPropertyPatternClause(CS.SyntaxFactory.PropertyPatternClause()) + .WithDesignation(CS.SyntaxFactory.SingleVariableDesignation(receiver.Identifier))); + if (whenNotNull is StatementSyntax || node.Parent is VBSyntax.ExpressionStatementSyntax || + _semanticModel.GetTypeInfo(node).Type?.SpecialType == SpecialType.System_Void) { + return CS.SyntaxFactory.IfStatement(condition, CS.SyntaxFactory.Block( + whenNotNull as StatementSyntax ?? CS.SyntaxFactory.ExpressionStatement((ExpressionSyntax)whenNotNull))); + } + return CS.SyntaxFactory.ParenthesizedExpression(CS.SyntaxFactory.ConditionalExpression(condition, (ExpressionSyntax)whenNotNull, + CS.SyntaxFactory.DefaultExpression(CommonConversions.GetTypeSyntax(_semanticModel.GetTypeInfo(node).Type)))); + } finally { + _conditionalAccessReceivers.Remove(node); + } + } return CS.SyntaxFactory.ConditionalAccessExpression(leftExpression, await node.WhenNotNull.AcceptAsync(TriviaConvertingExpressionVisitor)); } diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index d008b8ae5..d1c94a807 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -199,7 +199,8 @@ public override async Task> VisitExpressionStatement return new SyntaxList(); } - return SingleStatement(await node.Expression.AcceptAsync(_expressionVisitor)); + var converted = await node.Expression.AcceptAsync(_expressionVisitor); + return converted is StatementSyntax statement ? SingleStatement(statement) : SingleStatement((ExpressionSyntax)converted); } public override async Task> VisitAssignmentStatement(VBSyntax.AssignmentStatementSyntax node) diff --git a/CodeConverter/CSharp/NameExpressionNodeVisitor.cs b/CodeConverter/CSharp/NameExpressionNodeVisitor.cs index 8a0fefd04..0da54a9fa 100644 --- a/CodeConverter/CSharp/NameExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/NameExpressionNodeVisitor.cs @@ -19,6 +19,7 @@ internal class NameExpressionNodeVisitor private readonly HashSet _extraUsingDirectives; private readonly Dictionary> _tempNameForAnonymousScope; private readonly Stack _withBlockLhs; + private readonly Dictionary _conditionalAccessReceivers; private readonly ArgumentConverter _argumentConverter; public CommonConversions CommonConversions { get; } @@ -27,7 +28,8 @@ internal class NameExpressionNodeVisitor public NameExpressionNodeVisitor(SemanticModel semanticModel, HashSet generatedNames, ITypeContext typeContext, HashSet extraUsingDirectives, Dictionary> tempNameForAnonymousScope, Stack withBlockLhs, CommonConversions commonConversions, ArgumentConverter argumentConverter, - CommentConvertingVisitorWrapper triviaConvertingExpressionVisitor) + CommentConvertingVisitorWrapper triviaConvertingExpressionVisitor, + Dictionary conditionalAccessReceivers) { _semanticModel = semanticModel; _generatedNames = generatedNames; @@ -35,6 +37,7 @@ public NameExpressionNodeVisitor(SemanticModel semanticModel, HashSet ge _extraUsingDirectives = extraUsingDirectives; _tempNameForAnonymousScope = tempNameForAnonymousScope; _withBlockLhs = withBlockLhs; + _conditionalAccessReceivers = conditionalAccessReceivers; _argumentConverter = argumentConverter; CommonConversions = commonConversions; TriviaConvertingExpressionVisitor = triviaConvertingExpressionVisitor; @@ -84,13 +87,15 @@ public async Task ConvertMemberAccessExpressionAsync(VBasic.Sy } } if (left == null) { - if (IsSubPartOfConditionalAccess(node)) { - return isDefaultProperty ? SyntaxFactory.ElementBindingExpression() - : await AdjustForImplicitInvocationAsync(node, SyntaxFactory.MemberBindingExpression(simpleNameSyntax)); + if (GetConditionalAccess(node) is { } conditional) { + if (!_conditionalAccessReceivers.TryGetValue(conditional, out left)) { + return isDefaultProperty ? SyntaxFactory.ElementBindingExpression() + : await AdjustForImplicitInvocationAsync(node, SyntaxFactory.MemberBindingExpression(simpleNameSyntax)); + } } else if (node.IsParentKind(VBasic.SyntaxKind.NamedFieldInitializer) && _tempNameForAnonymousScope.TryGetValue(node.Name.Identifier.Text, out var stack) && stack.Any()) { return ValidSyntaxFactory.IdentifierName(stack.Peek().TempName); } - left = _withBlockLhs.Peek(); + left ??= _withBlockLhs.Peek(); } if (node.IsKind(VBasic.SyntaxKind.DictionaryAccessExpression)) { @@ -207,7 +212,10 @@ public async Task ConvertInvocationExpressionAsync( if (node.Expression is null) { var convertArgumentListOrEmptyAsync = await _argumentConverter.ConvertArgumentsAsync(node.ArgumentList); - return SyntaxFactory.ElementBindingExpression(SyntaxFactory.BracketedArgumentList(SyntaxFactory.SeparatedList(convertArgumentListOrEmptyAsync))); + var arguments = SyntaxFactory.BracketedArgumentList(SyntaxFactory.SeparatedList(convertArgumentListOrEmptyAsync)); + return GetConditionalAccess(node) is { } conditional && _conditionalAccessReceivers.TryGetValue(conditional, out var receiver) + ? SyntaxFactory.ElementAccessExpression(receiver, arguments) + : SyntaxFactory.ElementBindingExpression(arguments); } var convertedInvocation = await ConvertOrReplaceInvocationAsync(node, invocationSymbol); @@ -708,21 +716,21 @@ private static QualifiedNameSyntax Qualify(string qualification, ExpressionSynta (SimpleNameSyntax)toBeQualified); } - private static bool IsSubPartOfConditionalAccess(VBasic.Syntax.MemberAccessExpressionSyntax node) - { - static bool IsMemberAccessChain(SyntaxNode exp) => - exp?.IsKind(VBasic.SyntaxKind.InvocationExpression, - VBasic.SyntaxKind.SimpleMemberAccessExpression, - VBasic.SyntaxKind.ParenthesizedExpression, - VBasic.SyntaxKind.ConditionalAccessExpression) == true; + internal static bool IsMemberAccessChain(SyntaxNode exp) => + exp is VBSyntax.XmlMemberAccessExpressionSyntax || exp?.IsKind(VBasic.SyntaxKind.InvocationExpression, + VBasic.SyntaxKind.SimpleMemberAccessExpression, + VBasic.SyntaxKind.ParenthesizedExpression, + VBasic.SyntaxKind.ConditionalAccessExpression) == true; + internal static VBSyntax.ConditionalAccessExpressionSyntax GetConditionalAccess(SyntaxNode node) + { for (SyntaxNode child = node, parent = node.Parent; IsMemberAccessChain(parent); child = parent, parent = parent.Parent) { if (parent is VBSyntax.ConditionalAccessExpressionSyntax cae && cae.WhenNotNull == child) { - return true; // On right hand side of a ?. conditional access + return cae; } } - return false; + return null; } private static CSharpSyntaxNode ReplaceRightmostIdentifierText(CSharpSyntaxNode expr, SyntaxToken idToken, string overrideIdentifier) diff --git a/CodeConverter/CSharp/XmlExpressionConverter.cs b/CodeConverter/CSharp/XmlExpressionConverter.cs index a906d46be..6bf9b2ae7 100644 --- a/CodeConverter/CSharp/XmlExpressionConverter.cs +++ b/CodeConverter/CSharp/XmlExpressionConverter.cs @@ -1,4 +1,5 @@ using System.Xml.Linq; +using ICSharpCode.CodeConverter.Util.FromRoslyn; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -9,12 +10,24 @@ internal class XmlExpressionConverter { private readonly XmlImportContext _xmlImportContext; private readonly HashSet _extraUsingDirectives; - - public XmlExpressionConverter(XmlImportContext xmlImportContext, HashSet extraUsingDirectives, CommentConvertingVisitorWrapper triviaConvertingExpressionVisitor) + private readonly SemanticModel _semanticModel; + private readonly Stack _withBlockLhs; + private readonly HashSet _generatedNames; + private readonly Dictionary _conditionalAccessReceivers; + private readonly CommonConversions _commonConversions; + + public XmlExpressionConverter(XmlImportContext xmlImportContext, HashSet extraUsingDirectives, CommonConversions commonConversions, + SemanticModel semanticModel, Stack withBlockLhs, HashSet generatedNames, + Dictionary conditionalAccessReceivers) { _xmlImportContext = xmlImportContext; _extraUsingDirectives = extraUsingDirectives; - TriviaConvertingExpressionVisitor = triviaConvertingExpressionVisitor; + _commonConversions = commonConversions; + TriviaConvertingExpressionVisitor = commonConversions.TriviaConvertingExpressionVisitor; + _semanticModel = semanticModel; + _withBlockLhs = withBlockLhs; + _generatedNames = generatedNames; + _conditionalAccessReceivers = conditionalAccessReceivers; } public async Task ConvertXmlEmbeddedExpressionAsync(VBSyntax.XmlEmbeddedExpressionSyntax node) => await node.Expression.AcceptAsync(TriviaConvertingExpressionVisitor); @@ -84,23 +97,70 @@ public async Task ConvertXmlMemberAccessExpressionAsync( { _extraUsingDirectives.Add("System.Xml.Linq"); + if (node.Token2.Text == "@") return await ConvertAttributeAxisAsync(node); + var xElementMethodName = GetXElementMethodName(node); - ExpressionSyntax elements = node.Base != null - ? SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - await node.Base.AcceptAsync(TriviaConvertingExpressionVisitor), - ValidSyntaxFactory.IdentifierName(xElementMethodName) - ) - : SyntaxFactory.MemberBindingExpression( - ValidSyntaxFactory.IdentifierName(xElementMethodName) - ); + var elements = MemberAccess(await ConvertReceiverAsync(node), xElementMethodName); return SyntaxFactory.InvocationExpression(elements, ExpressionSyntaxExtensions.CreateArgList( await node.Name.AcceptAsync(TriviaConvertingExpressionVisitor)) ); } + private async Task ConvertAttributeAxisAsync(VBSyntax.XmlMemberAccessExpressionSyntax node) + { + var conditional = node.Base == null ? NameExpressionNodeVisitor.GetConditionalAccess(node) : null; + var receiverSyntax = node.Base ?? conditional?.Expression ?? node.GetAncestor()?.WithStatement.Expression; + var receiverType = _semanticModel.GetTypeInfo(receiverSyntax).Type; + var receiver = await ConvertReceiverAsync(node); + var name = await node.Name.AcceptAsync(TriviaConvertingExpressionVisitor); + + var xElementType = _semanticModel.Compilation.GetTypeByMetadataName("System.Xml.Linq.XElement"); + if (receiverType.InheritsFromOrEquals(xElementType)) { + receiver = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(_commonConversions.GetTypeSyntax(xElementType), receiver)); + return AttributeValue(receiver, name); + } + + _extraUsingDirectives.Add("System.Linq"); + if (receiver != null) { + _extraUsingDirectives.Add("System"); + receiver = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, SyntaxFactory.ParenthesizedExpression(receiver), + SyntaxFactory.ThrowExpression(SyntaxFactory.ObjectCreationExpression(_commonConversions.GetTypeSyntax( + _semanticModel.Compilation.GetTypeByMetadataName("System.NullReferenceException"))) + .WithArgumentList(SyntaxFactory.ArgumentList())))); + } + var parameter = ValidSyntaxFactory.IdentifierName(NameGenerator.CS.GetUniqueVariableNameInScope(_semanticModel, _generatedNames, node, "element")); + var selector = SyntaxFactory.SimpleLambdaExpression(SyntaxFactory.Parameter(parameter.Identifier), AttributeValue(parameter, name)); + var enumerable = _commonConversions.GetTypeSyntax(_semanticModel.Compilation.GetTypeByMetadataName("System.Linq.Enumerable")); + var selectMethod = SyntaxFactory.GenericName("Select").WithTypeArgumentList(SyntaxFactory.TypeArgumentList( + SyntaxFactory.SeparatedList(new[] { + _commonConversions.GetTypeSyntax(xElementType), + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)) + }))); + var select = SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, enumerable, selectMethod), + new[] { receiver, selector }.CreateCsArgList()); + return SyntaxFactory.InvocationExpression(MemberAccess(enumerable, "FirstOrDefault"), select.Yield().CreateCsArgList()); + } + + private async Task ConvertReceiverAsync(VBSyntax.XmlMemberAccessExpressionSyntax node) + { + if (node.Base != null) return await node.Base.AcceptAsync(TriviaConvertingExpressionVisitor); + if (NameExpressionNodeVisitor.GetConditionalAccess(node) is { } conditional) { + return _conditionalAccessReceivers.TryGetValue(conditional, out var receiver) ? receiver : null; + } + return _withBlockLhs.Peek(); + } + + private static ExpressionSyntax AttributeValue(ExpressionSyntax receiver, ExpressionSyntax name) => + SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression( + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), + SyntaxFactory.InvocationExpression(MemberAccess(receiver, "Attribute"), name.Yield().CreateCsArgList()))); + + private static ExpressionSyntax MemberAccess(ExpressionSyntax receiver, string member) => receiver == null + ? SyntaxFactory.MemberBindingExpression(ValidSyntaxFactory.IdentifierName(member)) + : SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, receiver, ValidSyntaxFactory.IdentifierName(member)); + public Task ConvertXmlBracketedNameAsync(VBSyntax.XmlBracketedNameSyntax node) { return node.Name.AcceptAsync(TriviaConvertingExpressionVisitor); @@ -134,7 +194,10 @@ public async Task ConvertXmlNameAsync(VBSyntax.XmlNameSyntax n } } - if (_xmlImportContext.HasDefaultImport && node.Parent is not VBSyntax.XmlAttributeSyntax) { + var nameParent = node.Parent is VBSyntax.XmlBracketedNameSyntax ? node.Parent.Parent : node.Parent; + var isAttributeName = nameParent is VBSyntax.XmlAttributeSyntax || + nameParent is VBSyntax.XmlMemberAccessExpressionSyntax axis && axis.Token2.Text == "@"; + if (_xmlImportContext.HasDefaultImport && !isAttributeName) { return SyntaxFactory.BinaryExpression( SyntaxKind.AddExpression, SyntaxFactory.MemberAccessExpression( @@ -165,10 +228,6 @@ private static string GetXElementMethodName(VBSyntax.XmlMemberAccessExpressionSy return "Elements"; } - if (node.Token2.Text == "@") { - return "Attributes"; - } - if (node.Token2.Text == ".") { return "Descendants"; } diff --git a/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs b/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs index d059ca7cf..2821c651f 100644 --- a/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs @@ -1,4 +1,7 @@ using System.Threading.Tasks; +using System.IO; +using ICSharpCode.CodeConverter.Common; +using ICSharpCode.CodeConverter.CSharp; using ICSharpCode.CodeConverter.Tests.TestRunners; using Xunit; @@ -9,6 +12,15 @@ namespace ICSharpCode.CodeConverter.Tests.CSharp.ExpressionTests; /// public class XmlExpressionTests : ConverterTestBase { + [Fact] + public async Task AttributeAxisRuntimeEquivalenceAsync() + { + var path = Path.Combine(TestConstants.GetTestDataDirectory(), "SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb"); + foreach (var test in SelfVerifyingTestFactory.GetSelfVerifyingFacts(path)) { + await test.Execute(); + } + } + [Fact] public async Task NestedXmlEchoSimpleAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb new file mode 100644 index 000000000..a91b4e2f3 --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb @@ -0,0 +1,224 @@ +Imports System +Imports System.Collections.Generic +Imports System.Linq +Imports System.Xml.Linq +Imports Xunit +Imports +Imports + +Public Class XmlAttributeAxisTests + + Sub SingleElementAttributeIsAString() + Dim element = New XElement("item", New XAttribute("id", "first & ""quoted""")) + Dim value = element.@id + Assert.IsType(Of String)(value) + Assert.Equal("first & ""quoted""", value) + Assert.Equal("FIRST & ""QUOTED""", value.ToUpperInvariant()) + End Sub + + + Sub MissingAndEmptyAttributesAreDistinct() + Dim element = + Assert.Equal("", element.@id) + Assert.Null(element.@missing) + End Sub + + + Sub SequenceReadsOnlyTheFirstElement() + Dim elements As IEnumerable(Of XElement) = {, } + Assert.Equal("first", elements.@id) + End Sub + + + Sub SequenceDoesNotSearchLaterElementsForAttribute() + Dim elements As IEnumerable(Of XElement) = {, } + Assert.Null(elements.@id) + End Sub + + + Sub EmptySequenceReturnsNothing() + Dim elements As IEnumerable(Of XElement) = Enumerable.Empty(Of XElement)() + Assert.Null(elements.@id) + End Sub + + + Sub NullFirstElementThrows() + Dim elements As IEnumerable(Of XElement) = {Nothing, } + Assert.Throws(Of NullReferenceException)(Function() elements.@id) + End Sub + + + Sub NullReceiversThrow() + Dim element As XElement = Nothing + Dim elements As IEnumerable(Of XElement) = Nothing + Assert.Throws(Of NullReferenceException)(Function() element.@id) + Assert.Throws(Of NullReferenceException)(Function() elements.@id) + End Sub + + + Sub AttributeNamespacesAreIndependentOfDefaultElementNamespace() + Dim element = + Assert.Equal("plain", element.@id) + Assert.Equal("qualified", element.@t:id) + Assert.Equal("en", element.@xml:lang) + Assert.Equal("plain", element.@) + Assert.Equal("qualified", element.@) + Assert.Equal("qualified", {element}.@t:id) + Assert.Equal("integer", element.@) + End Sub + + + Sub ConditionalAttributeAccess() + Dim element As XElement = Nothing + Dim elements As IEnumerable(Of XElement) = Nothing + Assert.Null(element?.@id) + Assert.Null(elements?.@id) + element = + elements = {element} + Assert.Equal("present", element?.@id) + Assert.Equal("present", elements?.@id) + End Sub + + + Sub AttributeAccessInWithBlock() + With + Assert.Equal("present", .@id) + End With + End Sub + + + Sub AttributeAccessAfterConditionalChildAxis() + Dim root = + Assert.Equal("first", root?..@id) + root = Nothing + Assert.Null(root?..@id) + End Sub + + + Sub AttributeAccessAfterConditionalIndex() + Dim elements As XElement() = {} + Assert.Equal("first", elements?(0).@id) + elements = Nothing + Assert.Null(elements?(0).@id) + End Sub + + + Sub WithAttributeInsideConditionalLambda() + Dim values As IEnumerable(Of Integer) = {1} + With + Assert.Equal("present", values?.Select(Function(i) .@id).Single()) + End With + End Sub + + + Sub QueryableReceiverUsesEnumerableSemantics() + Dim elements As IQueryable(Of XElement) = {}.AsQueryable() + Assert.Equal("first", elements.@id) + Assert.Equal("first", elements?.@id) + End Sub + + + Sub MissingAttributeDoesNotShortCircuitFollowingMemberAccess() + Dim element = + Assert.Throws(Of NullReferenceException)(Function() element.@id.Length) + Assert.Throws(Of NullReferenceException)(Function() element?.@id.Length) + element = Nothing + Assert.Null(element?.@id.Length) + End Sub + + + Sub ConditionalAttributeFollowedByVoidMethod() + Dim element = + Dim chars(2) As Char + element?.@id.CopyTo(0, chars, 0, 3) + element?.@id.ToUpperInvariant() + Assert.Equal("abc", New String(chars)) + element = Nothing + element?.@id.CopyTo(0, chars, 0, 3) + Assert.Equal("abc", New String(chars)) + End Sub + + + Sub ConditionalIntermediateSequenceAndNestedConditionalAccess() + Dim holder = New ElementHolder() + Assert.Throws(Of NullReferenceException)(Function() holder?.Items.@id) + Assert.Null(holder?.Items?.@id) + holder.Items = {} + Assert.Equal("present", holder?.Items.@id) + Assert.Equal("present", holder?.Items?.@id) + holder = Nothing + Assert.Null(holder?.Items.@id) + Assert.Null(holder?.Items?.@id) + End Sub + + + Sub SequenceIgnoresCustomAsEnumerableMethod() + Dim elements = New ElementList() From {} + Assert.Equal("first", elements.@id) + Assert.Equal("first", elements?.@id) + End Sub + + + Sub ElementIgnoresShadowedAttributeMethod() + Dim element = New DerivedElement() + Assert.Equal("first", element.@id) + Assert.Equal("first", element?.@id) + Dim elements As IEnumerable(Of DerivedElement) = {element} + Assert.Equal("first", elements.@id) + End Sub + + + Sub ReceiverIsEvaluatedOnce() + Dim calls = 0 + Dim getElement As Func(Of XElement) = Function() + calls += 1 + Return + End Function + Assert.Equal("present", getElement().@id) + Assert.Equal(1, calls) + Assert.Equal("present", getElement()?.@id) + Assert.Equal(2, calls) + Dim getElements As Func(Of IEnumerable(Of XElement)) = Function() + calls += 1 + Return {} + End Function + Assert.Equal("present", getElements().@id) + Assert.Equal(3, calls) + Assert.Equal("present", getElements()?.@id) + Assert.Equal(4, calls) + End Sub + + + Sub SequenceDoesNotAdvancePastFirstElement() + Assert.Equal("first", FirstElementOnly().@id) + End Sub + + Private Iterator Function FirstElementOnly() As IEnumerable(Of XElement) + Yield + Throw New InvalidOperationException("The second element must not be read.") + End Function + + Private Class ElementHolder + Public Property Items As IEnumerable(Of XElement) + End Class + + Private Class ElementList + Inherits List(Of XElement) + + Public Function AsEnumerable() As IEnumerable(Of XElement) + Throw New InvalidOperationException("Attribute access must enumerate the receiver directly.") + End Function + End Class + + Private Class DerivedElement + Inherits XElement + + Public Sub New() + MyBase.New("item", New XAttribute("id", "first")) + End Sub + + Public Shadows Function Attribute(name As XName) As XAttribute + Throw New InvalidOperationException("Attribute access must use XElement.Attribute.") + End Function + End Class +End Class From ae1d57ca8b0ffa7eadedab2dc9acc83d6418ec3b Mon Sep 17 00:00:00 2001 From: Travis Jones <31292511+CosmonautJones@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:51:25 -0400 Subject: [PATCH 2/3] Preserve explicit Call and value-type XML receivers --- .../MethodBodyExecutableStatementVisitor.cs | 3 +- .../CSharp/XmlExpressionConverter.cs | 5 ++- .../VBToCS/XmlAttributeAxisTests.vb | 31 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index d1c94a807..02e88f399 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -1225,7 +1225,8 @@ await stmt.Condition.AcceptAsync(_expressionVisitor), public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) { - return SingleStatement(await node.Invocation.AcceptAsync(_expressionVisitor)); + var converted = await node.Invocation.AcceptAsync(_expressionVisitor); + return converted is StatementSyntax statement ? SingleStatement(statement) : SingleStatement((ExpressionSyntax)converted); } private static SyntaxList SingleStatement(StatementSyntax statement) diff --git a/CodeConverter/CSharp/XmlExpressionConverter.cs b/CodeConverter/CSharp/XmlExpressionConverter.cs index 6bf9b2ae7..4298ffa3a 100644 --- a/CodeConverter/CSharp/XmlExpressionConverter.cs +++ b/CodeConverter/CSharp/XmlExpressionConverter.cs @@ -125,7 +125,10 @@ private async Task ConvertAttributeAxisAsync(VBSyntax.XmlMembe _extraUsingDirectives.Add("System.Linq"); if (receiver != null) { _extraUsingDirectives.Add("System"); - receiver = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, SyntaxFactory.ParenthesizedExpression(receiver), + _extraUsingDirectives.Add("System.Collections.Generic"); + var sequenceType = _semanticModel.Compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1").Construct(xElementType); + var sequence = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(_commonConversions.GetTypeSyntax(sequenceType), receiver)); + receiver = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.BinaryExpression(SyntaxKind.CoalesceExpression, sequence, SyntaxFactory.ThrowExpression(SyntaxFactory.ObjectCreationExpression(_commonConversions.GetTypeSyntax( _semanticModel.Compilation.GetTypeByMetadataName("System.NullReferenceException"))) .WithArgumentList(SyntaxFactory.ArgumentList())))); diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb index a91b4e2f3..b9ba91acb 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb @@ -138,6 +138,19 @@ Public Class XmlAttributeAxisTests Assert.Equal("abc", New String(chars)) End Sub + + Sub ExplicitCallAfterConditionalAttributeAccess() + Dim element = + Dim chars(2) As Char + Call element?.@id.CopyTo(0, chars, 0, 3) + Call element?.@id.ToUpperInvariant() + Assert.Equal("abc", New String(chars)) + element = Nothing + Call element?.@id.CopyTo(0, chars, 0, 3) + Call element?.@id.ToUpperInvariant() + Assert.Equal("abc", New String(chars)) + End Sub + Sub ConditionalIntermediateSequenceAndNestedConditionalAccess() Dim holder = New ElementHolder() @@ -158,6 +171,12 @@ Public Class XmlAttributeAxisTests Assert.Equal("first", elements?.@id) End Sub + + Sub ValueTypeSequenceUsesEnumerableSemantics() + Dim elements = New ElementSequence() + Assert.Equal("first", elements.@id) + End Sub + Sub ElementIgnoresShadowedAttributeMethod() Dim element = New DerivedElement() @@ -202,6 +221,18 @@ Public Class XmlAttributeAxisTests Public Property Items As IEnumerable(Of XElement) End Class + Private Structure ElementSequence + Implements IEnumerable(Of XElement) + + Public Function GetEnumerator() As IEnumerator(Of XElement) Implements IEnumerable(Of XElement).GetEnumerator + Return New List(Of XElement) From {}.GetEnumerator() + End Function + + Private Function GetUntypedEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator + Return GetEnumerator() + End Function + End Structure + Private Class ElementList Inherits List(Of XElement) From 0873fce22ee2e6299c5955e5d51914009f836560 Mon Sep 17 00:00:00 2001 From: Travis Jones <31292511+CosmonautJones@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:15:24 -0400 Subject: [PATCH 3/3] Refactor XML statement dispatch and rely on automatic test discovery --- .../CSharp/MethodBodyExecutableStatementVisitor.cs | 12 ++++++------ Tests/CSharp/ExpressionTests/XmlExpressionTests.cs | 12 ------------ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 02e88f399..6e9720622 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -199,8 +199,7 @@ public override async Task> VisitExpressionStatement return new SyntaxList(); } - var converted = await node.Expression.AcceptAsync(_expressionVisitor); - return converted is StatementSyntax statement ? SingleStatement(statement) : SingleStatement((ExpressionSyntax)converted); + return SingleStatement(await node.Expression.AcceptAsync(_expressionVisitor)); } public override async Task> VisitAssignmentStatement(VBSyntax.AssignmentStatementSyntax node) @@ -1225,8 +1224,7 @@ await stmt.Condition.AcceptAsync(_expressionVisitor), public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) { - var converted = await node.Invocation.AcceptAsync(_expressionVisitor); - return converted is StatementSyntax statement ? SingleStatement(statement) : SingleStatement((ExpressionSyntax)converted); + return SingleStatement(await node.Invocation.AcceptAsync(_expressionVisitor)); } private static SyntaxList SingleStatement(StatementSyntax statement) @@ -1234,8 +1232,10 @@ private static SyntaxList SingleStatement(StatementSyntax state return SyntaxFactory.SingletonList(statement); } - private static SyntaxList SingleStatement(ExpressionSyntax expression) + private static SyntaxList SingleStatement(CSharpSyntaxNode node) { - return SyntaxFactory.SingletonList(SyntaxFactory.ExpressionStatement(expression)); + return SingleStatement(node is StatementSyntax statement + ? statement + : SyntaxFactory.ExpressionStatement((ExpressionSyntax)node)); } } \ No newline at end of file diff --git a/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs b/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs index 2821c651f..d059ca7cf 100644 --- a/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/XmlExpressionTests.cs @@ -1,7 +1,4 @@ using System.Threading.Tasks; -using System.IO; -using ICSharpCode.CodeConverter.Common; -using ICSharpCode.CodeConverter.CSharp; using ICSharpCode.CodeConverter.Tests.TestRunners; using Xunit; @@ -12,15 +9,6 @@ namespace ICSharpCode.CodeConverter.Tests.CSharp.ExpressionTests; /// public class XmlExpressionTests : ConverterTestBase { - [Fact] - public async Task AttributeAxisRuntimeEquivalenceAsync() - { - var path = Path.Combine(TestConstants.GetTestDataDirectory(), "SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb"); - foreach (var test in SelfVerifyingTestFactory.GetSelfVerifyingFacts(path)) { - await test.Execute(); - } - } - [Fact] public async Task NestedXmlEchoSimpleAsync() {