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..6e9720622 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -199,7 +199,7 @@ public override async Task> VisitExpressionStatement return new SyntaxList(); } - return SingleStatement(await node.Expression.AcceptAsync(_expressionVisitor)); + return SingleStatement(await node.Expression.AcceptAsync(_expressionVisitor)); } public override async Task> VisitAssignmentStatement(VBSyntax.AssignmentStatementSyntax node) @@ -1224,7 +1224,7 @@ await stmt.Condition.AcceptAsync(_expressionVisitor), public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) { - return SingleStatement(await node.Invocation.AcceptAsync(_expressionVisitor)); + return SingleStatement(await node.Invocation.AcceptAsync(_expressionVisitor)); } private static SyntaxList SingleStatement(StatementSyntax statement) @@ -1232,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/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..4298ffa3a 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,73 @@ 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"); + _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())))); + } + 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 +197,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 +231,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/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb new file mode 100644 index 000000000..b9ba91acb --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/XmlAttributeAxisTests.vb @@ -0,0 +1,255 @@ +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 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() + 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 ValueTypeSequenceUsesEnumerableSemantics() + Dim elements = New ElementSequence() + 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 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) + + 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