diff --git a/ICSharpCode.CodeConverter/CSharp/CommonConversions.cs b/ICSharpCode.CodeConverter/CSharp/CommonConversions.cs index 3dae9f67e..ac0e7a323 100644 --- a/ICSharpCode.CodeConverter/CSharp/CommonConversions.cs +++ b/ICSharpCode.CodeConverter/CSharp/CommonConversions.cs @@ -19,6 +19,7 @@ using TypeSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax; using VariableDeclaratorSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.VariableDeclaratorSyntax; using VisualBasicExtensions = Microsoft.CodeAnalysis.VisualBasic.VisualBasicExtensions; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; namespace ICSharpCode.CodeConverter.CSharp { @@ -346,7 +347,7 @@ private SyntaxToken VisualBasicDefaultVisibility(TokenContext context, bool isVa } internal SyntaxList ConvertArrayRankSpecifierSyntaxes( - SyntaxList arrayRankSpecifierSyntaxs, + SyntaxList arrayRankSpecifierSyntaxs, ArgumentListSyntax nodeArrayBounds, bool withSizes = true) { var bounds = SyntaxFactory.List(arrayRankSpecifierSyntaxs.Select(r => (ArrayRankSpecifierSyntax)r.Accept(_nodesVisitor))); @@ -372,7 +373,7 @@ public IEnumerable ConvertArrayBounds(ArgumentListSyntax argum return argumentListSyntax.Arguments.Select(a => IncreaseArrayUpperBoundExpression(((SimpleArgumentSyntax)a).Expression)); } - private ExpressionSyntax IncreaseArrayUpperBoundExpression(Microsoft.CodeAnalysis.VisualBasic.Syntax.ExpressionSyntax expr) + private ExpressionSyntax IncreaseArrayUpperBoundExpression(VBSyntax.ExpressionSyntax expr) { var constant = _semanticModel.GetConstantValue(expr); if (constant.HasValue && constant.Value is int) diff --git a/ICSharpCode.CodeConverter/CSharp/NodesVisitor.cs b/ICSharpCode.CodeConverter/CSharp/NodesVisitor.cs index 86f389dcb..f59fe9994 100644 --- a/ICSharpCode.CodeConverter/CSharp/NodesVisitor.cs +++ b/ICSharpCode.CodeConverter/CSharp/NodesVisitor.cs @@ -1,19 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; using ICSharpCode.CodeConverter.Shared; using ICSharpCode.CodeConverter.Util; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using ISymbolExtensions = ICSharpCode.CodeConverter.Util.ISymbolExtensions; using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; using VBasic = Microsoft.CodeAnalysis.VisualBasic; -using static ICSharpCode.CodeConverter.CSharp.SyntaxKindExtensions; -using SyntaxExtensions = ICSharpCode.CodeConverter.Util.SyntaxExtensions; -using SyntaxNodeExtensions = ICSharpCode.CodeConverter.Util.SyntaxNodeExtensions; using SyntaxToken = Microsoft.CodeAnalysis.SyntaxToken; namespace ICSharpCode.CodeConverter.CSharp @@ -30,6 +24,7 @@ class NodesVisitor : VBasic.VisualBasicSyntaxVisitor private static readonly SyntaxToken SemicolonToken = SyntaxFactory.Token(SyntaxKind.SemicolonToken); private static readonly TypeSyntax VarType = SyntaxFactory.ParseTypeName("var"); private readonly AdditionalInitializers _additionalInitializers; + private readonly QueryConverter _queryConverter; public CommentConvertingNodesVisitor TriviaConvertingVisitor { get; } private CommonConversions CommonConversions { get; } @@ -40,6 +35,7 @@ public NodesVisitor(SemanticModel semanticModel) TriviaConvertingVisitor = new CommentConvertingNodesVisitor(this); _createConvertMethodsLookupByReturnType = CreateConvertMethodsLookupByReturnType(semanticModel); CommonConversions = new CommonConversions(semanticModel, TriviaConvertingVisitor); + _queryConverter = new QueryConverter(CommonConversions, TriviaConvertingVisitor); _additionalInitializers = new AdditionalInitializers(); } @@ -1210,79 +1206,7 @@ public override CSharpSyntaxNode VisitCollectionInitializer(VBSyntax.CollectionI public override CSharpSyntaxNode VisitQueryExpression(VBSyntax.QueryExpressionSyntax node) { - var vbBodyClauses = node.Clauses; - var vbFromClause = vbBodyClauses.OfType().First(); - var fromClauseSyntax = ConvertFromClauseSyntax(vbFromClause); - var vbGroupClause = vbBodyClauses.OfType().SingleOrDefault(); - var vbSelectClause = vbBodyClauses.OfType().SingleOrDefault(); - var selectClauseSyntax = vbSelectClause != null - ? ConvertSelectClauseSyntax(vbSelectClause) - : CreateDefaultSelectClause(fromClauseSyntax); - var alreadyConverted = new VBSyntax.QueryClauseSyntax[] { vbFromClause, vbGroupClause, vbSelectClause }; - SelectOrGroupClauseSyntax selectOrGroup = null; - QueryContinuationSyntax queryContinuation = null; - var queryClauseSyntaxs = SyntaxFactory.List(vbBodyClauses.TakeWhile(x => x != vbGroupClause).Except(alreadyConverted).Select(ConvertQueryBodyClause)); - var continuedClauses = SyntaxFactory.List(vbBodyClauses.SkipWhile(x => x != vbGroupClause).Skip(1).Except(alreadyConverted).Select(ConvertQueryBodyClause)); - if (vbGroupClause != null) { - selectOrGroup = ConvertGroupByClause(vbGroupClause); - queryContinuation = SyntaxFactory.QueryContinuation(GetGroupIdentifier(vbGroupClause), - SyntaxFactory.QueryBody(continuedClauses, selectClauseSyntax, null)); - } else { - selectOrGroup = selectClauseSyntax; - } - - var queryBodySyntax = SyntaxFactory.QueryBody(queryClauseSyntaxs, selectOrGroup, queryContinuation); - return SyntaxFactory.QueryExpression(fromClauseSyntax, queryBodySyntax); - } - - private FromClauseSyntax ConvertFromClauseSyntax(VBSyntax.FromClauseSyntax vbFromClause) - { - var collectionRangeVariableSyntax = vbFromClause.Variables.Single(); - var fromClauseSyntax = SyntaxFactory.FromClause( - ConvertIdentifier(collectionRangeVariableSyntax.Identifier.Identifier), - (ExpressionSyntax) collectionRangeVariableSyntax.Expression.Accept(TriviaConvertingVisitor)); - return fromClauseSyntax; - } - - private SelectClauseSyntax ConvertSelectClauseSyntax(VBSyntax.SelectClauseSyntax vbFromClause) - { - var collectionRangeVariableSyntax = vbFromClause.Variables.Single(); - return SyntaxFactory.SelectClause( - (ExpressionSyntax)collectionRangeVariableSyntax.Expression.Accept(TriviaConvertingVisitor) - ); - } - - private static SelectClauseSyntax CreateDefaultSelectClause(FromClauseSyntax fromClauseSyntax) - { - return SyntaxFactory.SelectClause(SyntaxFactory.IdentifierName(fromClauseSyntax.Identifier)); - } - - private QueryClauseSyntax ConvertQueryBodyClause(VBSyntax.QueryClauseSyntax node) - { - return node.TypeSwitch( - //(VBSyntax.AggregateClauseSyntax ags) => null, - //(VBSyntax.DistinctClauseSyntax ds) => null, - ConvertFromClauseSyntax, - ConvertJoinClause, - ConvertLetClause, - ConvertOrderByClause, - //(VBSyntax.PartitionClauseSyntax ps) => null, // TODO Convert to Skip and Take methods (they don't exist in C#s query syntax) - //(VBSyntax.PartitionWhileClauseSyntax pws) => null, // TODO Convert to SkipWhile and TakeWhile methods (they don't exist in C#s query syntax) - ConvertWhereClause, - _ => throw new NotImplementedException($"Conversion for query clause with kind '{node.Kind()}' not implemented")); - } - - private GroupClauseSyntax ConvertGroupByClause(VBSyntax.GroupByClauseSyntax gs) - { - if (gs == null) return null; - var item = (IdentifierNameSyntax) gs.Items.Single().Expression.Accept(TriviaConvertingVisitor); - var keyExpression = (ExpressionSyntax) gs.Keys.Single().Expression.Accept(TriviaConvertingVisitor); - return SyntaxFactory.GroupClause(item, keyExpression); - } - - private SyntaxToken GetGroupIdentifier(VBSyntax.GroupByClauseSyntax gs) - { - return ConvertIdentifier(gs.AggregationVariables.Select(x => x.Aggregation).OfType().First().FunctionName); + return _queryConverter.ConvertClauses(node.Clauses); } private SyntaxToken ConvertIdentifier(SyntaxToken identifierIdentifier, bool isAttribute = false) @@ -1290,22 +1214,6 @@ private SyntaxToken ConvertIdentifier(SyntaxToken identifierIdentifier, bool isA return CommonConversions.ConvertIdentifier(identifierIdentifier, isAttribute); } - private QueryClauseSyntax ConvertWhereClause(VBSyntax.WhereClauseSyntax ws) - { - return SyntaxFactory.WhereClause((ExpressionSyntax) ws.Condition.Accept(TriviaConvertingVisitor)); - } - - private QueryClauseSyntax ConvertLetClause(VBSyntax.LetClauseSyntax ls) - { - var singleVariable = ls.Variables.Single(); - return SyntaxFactory.LetClause(ConvertIdentifier(singleVariable.NameEquals.Identifier.Identifier), (ExpressionSyntax) singleVariable.Expression.Accept(TriviaConvertingVisitor)); - } - - private QueryClauseSyntax ConvertOrderByClause(VBSyntax.OrderByClauseSyntax os) - { - return SyntaxFactory.OrderByClause(SyntaxFactory.SeparatedList(os.Orderings.Select(o => (OrderingSyntax) o.Accept(TriviaConvertingVisitor)))); - } - public override CSharpSyntaxNode VisitOrdering(VBSyntax.OrderingSyntax node) { var convertToken = node.Kind().ConvertToken(); @@ -1314,34 +1222,6 @@ public override CSharpSyntaxNode VisitOrdering(VBSyntax.OrderingSyntax node) return SyntaxFactory.Ordering(convertToken, expressionSyntax, ascendingOrDescendingKeyword); } - private QueryClauseSyntax ConvertJoinClause(VBSyntax.JoinClauseSyntax js) - { - var variable = js.JoinedVariables.Single(); - var joinLhs = SingleExpression(js.JoinConditions.Select(c => c.Left.Accept(TriviaConvertingVisitor)) - .Cast().ToList()); - var joinRhs = SingleExpression(js.JoinConditions.Select(c => c.Right.Accept(TriviaConvertingVisitor)) - .Cast().ToList()); - var convertIdentifier = ConvertIdentifier(variable.Identifier.Identifier); - var expressionSyntax = (ExpressionSyntax) variable.Expression.Accept(TriviaConvertingVisitor); - - JoinIntoClauseSyntax joinIntoClauseSyntax = null; - if (js is VBSyntax.GroupJoinClauseSyntax gjs) { - joinIntoClauseSyntax = gjs.AggregationVariables - .Where(a => a.Aggregation is VBSyntax.GroupAggregationSyntax) - .Select(a => SyntaxFactory.JoinIntoClause(ConvertIdentifier(a.NameEquals.Identifier.Identifier))) - .SingleOrDefault(); - } - return SyntaxFactory.JoinClause(null, convertIdentifier, expressionSyntax, joinLhs, joinRhs, joinIntoClauseSyntax); - } - - private ExpressionSyntax SingleExpression(IReadOnlyCollection expressions) - { - if (expressions.Count == 1) return expressions.Single(); - return SyntaxFactory.AnonymousObjectCreationExpression(SyntaxFactory.SeparatedList(expressions.Select((e, i) => - SyntaxFactory.AnonymousObjectMemberDeclarator(SyntaxFactory.NameEquals($"key{i}"), e) - ))); - } - public override CSharpSyntaxNode VisitNamedFieldInitializer(VBSyntax.NamedFieldInitializerSyntax node) { if (node?.Parent?.Parent is VBSyntax.AnonymousObjectCreationExpressionSyntax) { @@ -1646,7 +1526,7 @@ private bool EnclosingTypeImplicitlyQualifiesSymbol(SyntaxNode syntaxNodeContext { ISymbol typeContext = syntaxNodeContext.GetEnclosingDeclaredTypeSymbol(_semanticModel); var implicitCsQualifications = ((ITypeSymbol) typeContext).GetBaseTypesAndThis() - .Concat(CSharpUtil.FollowProperty(typeContext, n => n.ContainingSymbol)) + .Concat(typeContext.FollowProperty(n => n.ContainingSymbol)) .ToList(); return implicitCsQualifications.Contains(symbolToCheck); diff --git a/ICSharpCode.CodeConverter/CSharp/QueryConverter.cs b/ICSharpCode.CodeConverter/CSharp/QueryConverter.cs new file mode 100644 index 000000000..e9582bfb7 --- /dev/null +++ b/ICSharpCode.CodeConverter/CSharp/QueryConverter.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ICSharpCode.CodeConverter.Util; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace ICSharpCode.CodeConverter.CSharp +{ + /// + /// Grammar info: http://kursinfo.himolde.no/in-kurs/IBE150/VBspec.htm#_Toc248253288 + /// + internal class QueryConverter + { + private readonly CommentConvertingNodesVisitor _triviaConvertingVisitor; + + public QueryConverter(CommonConversions commonConversions, CommentConvertingNodesVisitor triviaConvertingVisitor) + { + CommonConversions = commonConversions; + _triviaConvertingVisitor = triviaConvertingVisitor; + } + + private CommonConversions CommonConversions { get; } + + public CSharpSyntaxNode ConvertClauses(SyntaxList clauses) + { + var vbBodyClauses = new Queue(clauses); + var vbFromClause = (VBSyntax.FromClauseSyntax) vbBodyClauses.Dequeue(); + + var fromClauseSyntax = ConvertFromClauseSyntax(vbFromClause); + var querySegments = GetQuerySegments(vbBodyClauses); + return ConvertQuerySegments(querySegments, fromClauseSyntax); + } + + /// + /// TODO: Don't bother with reversing, rewrite ConvertQueryWithContinuation to recurse on them the right way around + /// + private IEnumerable<(Queue<(SyntaxList, VBSyntax.QueryClauseSyntax)>, VBSyntax.QueryClauseSyntax)> GetQuerySegments(Queue vbBodyClauses) + { + while (vbBodyClauses.Any()) { + var querySectionsReversed = + new Queue<(SyntaxList, VBSyntax.QueryClauseSyntax)>(); + while (vbBodyClauses.Any() && !RequiresMethodInvocation(vbBodyClauses.Peek())) { + var convertedClauses = new List(); + while (vbBodyClauses.Any() && !RequiredContinuation(vbBodyClauses.Peek())) { + convertedClauses.Add(ConvertQueryBodyClause(vbBodyClauses.Dequeue())); + } + + var convertQueryBodyClauses = (SyntaxFactory.List(convertedClauses), + vbBodyClauses.Any() ? vbBodyClauses.Dequeue() : null); + querySectionsReversed.Enqueue(convertQueryBodyClauses); + } + yield return (querySectionsReversed, vbBodyClauses.Any() ? vbBodyClauses.Dequeue() : null); + } + } + + private CSharpSyntaxNode ConvertQuerySegments(IEnumerable<(Queue<(SyntaxList, VBSyntax.QueryClauseSyntax)>, VBSyntax.QueryClauseSyntax)> querySegments, CSSyntax.FromClauseSyntax fromClauseSyntax) + { + CSSyntax.ExpressionSyntax query = null; + foreach (var (queryContinuation, queryEnd) in querySegments) { + query = (CSSyntax.ExpressionSyntax) ConvertQueryWithContinuations(queryContinuation, fromClauseSyntax); + if (queryEnd == null) return query; + var queryWithoutTrivia = ConvertQueryToLinq(fromClauseSyntax, queryEnd, query); + query = _triviaConvertingVisitor.TriviaConverter.PortConvertedTrivia(queryEnd, queryWithoutTrivia); + fromClauseSyntax = SyntaxFactory.FromClause(fromClauseSyntax.Identifier, query); + } + + return query ?? throw new ArgumentOutOfRangeException(nameof(querySegments), querySegments, null); + } + + private CSSyntax.InvocationExpressionSyntax ConvertQueryToLinq(CSSyntax.FromClauseSyntax fromClauseSyntax, VBSyntax.QueryClauseSyntax queryEnd, + CSSyntax.ExpressionSyntax query) + { + var linqMethodName = GetLinqMethodName(queryEnd); + var parenthesizedQuery = query is CSSyntax.QueryExpressionSyntax ? SyntaxFactory.ParenthesizedExpression(query) : query; + var linqMethod = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, parenthesizedQuery, + SyntaxFactory.IdentifierName(linqMethodName)); + var linqArguments = SyntaxFactory.ArgumentList( + SyntaxFactory.SeparatedList(GetLinqArguments(fromClauseSyntax, queryEnd).Select(SyntaxFactory.Argument))); + var invocationExpressionSyntax = SyntaxFactory.InvocationExpression(linqMethod, linqArguments); + return invocationExpressionSyntax; + } + + private CSharpSyntaxNode ConvertQueryWithContinuations(Queue<(SyntaxList, VBSyntax.QueryClauseSyntax)> queryContinuation, CSSyntax.FromClauseSyntax fromClauseSyntax) + { + var subQuery = ConvertQueryWithContinuation(queryContinuation, fromClauseSyntax); + return subQuery != null ? SyntaxFactory.QueryExpression(fromClauseSyntax, subQuery) : fromClauseSyntax.Expression; + } + + private CSSyntax.QueryBodySyntax ConvertQueryWithContinuation(Queue<(SyntaxList, VBSyntax.QueryClauseSyntax)> querySectionsReversed, CSSyntax.FromClauseSyntax fromClauseSyntax) + { + if (!querySectionsReversed.Any()) return null; + var (convertedClauses, clauseEnd) = querySectionsReversed.Dequeue(); + + var nestedClause = ConvertQueryWithContinuation(querySectionsReversed, fromClauseSyntax); + return ConvertSubQuery(fromClauseSyntax, clauseEnd, nestedClause, convertedClauses); + } + + private CSSyntax.QueryBodySyntax ConvertSubQuery(CSSyntax.FromClauseSyntax fromClauseSyntax, VBSyntax.QueryClauseSyntax clauseEnd, + CSSyntax.QueryBodySyntax nestedClause, SyntaxList convertedClauses) + { + CSSyntax.SelectOrGroupClauseSyntax selectOrGroup = null; + CSSyntax.QueryContinuationSyntax queryContinuation = null; + switch (clauseEnd) { + case null: + selectOrGroup = CreateDefaultSelectClause(fromClauseSyntax); + break; + case VBSyntax.GroupByClauseSyntax gcs: + var continuationClauses = SyntaxFactory.List(); + if (!gcs.Items.Any()) { + var identifierNameSyntax = + SyntaxFactory.IdentifierName(CommonConversions.ConvertIdentifier(fromClauseSyntax.Identifier)); + var letGroupKey = SyntaxFactory.LetClause(GetGroupKeyIdentifiers(gcs).Single(), SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName(GetGroupIdentifier(gcs)), SyntaxFactory.IdentifierName("Key"))); + continuationClauses = continuationClauses.Add(letGroupKey); + selectOrGroup = SyntaxFactory.GroupClause(identifierNameSyntax, GetGroupExpression(gcs)); + } else { + var item = (CSSyntax.IdentifierNameSyntax)gcs.Items.Single().Expression.Accept(_triviaConvertingVisitor); + var keyExpression = (CSSyntax.ExpressionSyntax)gcs.Keys.Single().Expression.Accept(_triviaConvertingVisitor); + selectOrGroup = SyntaxFactory.GroupClause(item, keyExpression); + } + queryContinuation = CreateGroupByContinuation(gcs, continuationClauses, nestedClause); + break; + case VBSyntax.SelectClauseSyntax scs: + selectOrGroup = ConvertSelectClauseSyntax(scs); + break; + default: + throw new NotImplementedException($"Clause kind '{clauseEnd.Kind()}' is not yet implemented"); + } + + return SyntaxFactory.QueryBody(convertedClauses, selectOrGroup, queryContinuation); + } + + private CSSyntax.QueryContinuationSyntax CreateGroupByContinuation(VBSyntax.GroupByClauseSyntax gcs, SyntaxList convertedClauses, CSSyntax.QueryBodySyntax body) + { + var queryBody = SyntaxFactory.QueryBody(convertedClauses, body?.SelectOrGroup, null); + return SyntaxFactory.QueryContinuation(GetGroupIdentifier(gcs), queryBody); + } + + private IEnumerable GetLinqArguments(CSSyntax.FromClauseSyntax fromClauseSyntax, + VBSyntax.QueryClauseSyntax linqQuery) + { + switch (linqQuery) { + case VBSyntax.DistinctClauseSyntax _: + return Enumerable.Empty(); + case VBSyntax.PartitionClauseSyntax pcs: + return new[] {(CSSyntax.ExpressionSyntax)pcs.Count.Accept(_triviaConvertingVisitor)}; + case VBSyntax.PartitionWhileClauseSyntax pwcs: { + var lambdaParam = SyntaxFactory.Parameter(fromClauseSyntax.Identifier); + var lambdaBody = (CSSyntax.ExpressionSyntax) pwcs.Condition.Accept(_triviaConvertingVisitor); + return new[] {(CSSyntax.ExpressionSyntax) SyntaxFactory.SimpleLambdaExpression(lambdaParam, lambdaBody)}; + } + default: + throw new ArgumentOutOfRangeException(nameof(linqQuery), linqQuery.Kind(), null); + } + } + + private static string GetLinqMethodName(VBSyntax.QueryClauseSyntax queryEnd) + { + switch (queryEnd) { + case VBSyntax.DistinctClauseSyntax _: + return nameof(Enumerable.Distinct); + case VBSyntax.PartitionClauseSyntax pcs: + return pcs.SkipOrTakeKeyword.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.SkipKeyword) ? nameof(Enumerable.Skip) : nameof(Enumerable.Take); + case VBSyntax.PartitionWhileClauseSyntax pwcs: + return pwcs.SkipOrTakeKeyword.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.SkipKeyword) ? nameof(Enumerable.SkipWhile) : nameof(Enumerable.TakeWhile); + default: + throw new ArgumentOutOfRangeException(nameof(queryEnd), queryEnd.Kind(), null); + } + } + + private static bool RequiresMethodInvocation(VBSyntax.QueryClauseSyntax queryClauseSyntax) + { + return queryClauseSyntax is VBSyntax.PartitionClauseSyntax + || queryClauseSyntax is VBSyntax.PartitionWhileClauseSyntax + || queryClauseSyntax is VBSyntax.DistinctClauseSyntax; + } + + private static bool RequiredContinuation(VBSyntax.QueryClauseSyntax queryClauseSyntax) + { + return queryClauseSyntax is VBSyntax.GroupByClauseSyntax + || queryClauseSyntax is VBSyntax.SelectClauseSyntax; + } + + private CSSyntax.FromClauseSyntax ConvertFromClauseSyntax(VBSyntax.FromClauseSyntax vbFromClause) + { + var collectionRangeVariableSyntax = vbFromClause.Variables.Single(); + var fromClauseSyntax = SyntaxFactory.FromClause( + CommonConversions.ConvertIdentifier(collectionRangeVariableSyntax.Identifier.Identifier), + (CSSyntax.ExpressionSyntax) collectionRangeVariableSyntax.Expression.Accept(_triviaConvertingVisitor)); + return fromClauseSyntax; + } + + private CSSyntax.SelectClauseSyntax ConvertSelectClauseSyntax(VBSyntax.SelectClauseSyntax vbFromClause) + { + var collectionRangeVariableSyntax = vbFromClause.Variables.Single(); + return SyntaxFactory.SelectClause( + (CSSyntax.ExpressionSyntax)collectionRangeVariableSyntax.Expression.Accept(_triviaConvertingVisitor) + ); + } + + /// + /// TODO: In the case of multiple Froms and no Select, VB returns an anonymous type containing all the variables created by the from clause + /// + private static CSSyntax.SelectClauseSyntax CreateDefaultSelectClause(CSSyntax.FromClauseSyntax fromClauseSyntax) + { + return SyntaxFactory.SelectClause(SyntaxFactory.IdentifierName(fromClauseSyntax.Identifier)); + } + + private CSSyntax.QueryClauseSyntax ConvertQueryBodyClause(VBSyntax.QueryClauseSyntax node) + { + return node.TypeSwitch( + //(VBSyntax.AggregateClauseSyntax ags) => null, + //(VBSyntax.DistinctClauseSyntax ds) => null, + ConvertFromClauseSyntax, + ConvertJoinClause, + ConvertLetClause, + ConvertOrderByClause, + //(VBSyntax.PartitionClauseSyntax ps) => null, // TODO Convert to Skip and Take methods (they don't exist in C#s query syntax) + //(VBSyntax.PartitionWhileClauseSyntax pws) => null, // TODO Convert to SkipWhile and TakeWhile methods (they don't exist in C#s query syntax) + ConvertWhereClause, + _ => throw new NotImplementedException($"Conversion for query clause with kind '{node.Kind()}' not implemented")); + } + + private CSSyntax.ExpressionSyntax GetGroupExpression(VBSyntax.GroupByClauseSyntax gs) + { + return (CSSyntax.ExpressionSyntax) gs.Keys.Single().Expression.Accept(_triviaConvertingVisitor); + } + + private SyntaxToken GetGroupIdentifier(VBSyntax.GroupByClauseSyntax gs) + { + var name = gs.AggregationVariables.Select(v => v.Aggregation is VBSyntax.FunctionAggregationSyntax f + ? f.FunctionName.Text : v.Aggregation is VBSyntax.GroupAggregationSyntax g ? g.GroupKeyword.Text : null) + .SingleOrDefault(x => x != null) ?? gs.Keys.Select(k => k.NameEquals.Identifier.Identifier.Text).Single(); + return SyntaxFactory.Identifier(name); + } + + private IEnumerable GetGroupKeyIdentifiers(VBSyntax.GroupByClauseSyntax gs) + { + return gs.AggregationVariables + .Select(x => x.NameEquals?.Identifier.Identifier.Text) + .Concat(gs.Keys.Select(k => k.NameEquals?.Identifier.Identifier.Text)) + .Where(x => x != null); + } + + private CSSyntax.QueryClauseSyntax ConvertWhereClause(VBSyntax.WhereClauseSyntax ws) + { + return SyntaxFactory.WhereClause((CSSyntax.ExpressionSyntax) ws.Condition.Accept(_triviaConvertingVisitor)); + } + + private CSSyntax.QueryClauseSyntax ConvertLetClause(VBSyntax.LetClauseSyntax ls) + { + var singleVariable = ls.Variables.Single(); + return SyntaxFactory.LetClause(CommonConversions.ConvertIdentifier(singleVariable.NameEquals.Identifier.Identifier), (CSSyntax.ExpressionSyntax) singleVariable.Expression.Accept(_triviaConvertingVisitor)); + } + + private CSSyntax.QueryClauseSyntax ConvertOrderByClause(VBSyntax.OrderByClauseSyntax os) + { + return SyntaxFactory.OrderByClause(SyntaxFactory.SeparatedList(os.Orderings.Select(o => (CSSyntax.OrderingSyntax) o.Accept(_triviaConvertingVisitor)))); + } + + private CSSyntax.QueryClauseSyntax ConvertJoinClause(VBSyntax.JoinClauseSyntax js) + { + var variable = js.JoinedVariables.Single(); + var joinLhs = SingleExpression(js.JoinConditions.Select(c => c.Left.Accept(_triviaConvertingVisitor)) + .Cast().ToList()); + var joinRhs = SingleExpression(js.JoinConditions.Select(c => c.Right.Accept(_triviaConvertingVisitor)) + .Cast().ToList()); + var convertIdentifier = CommonConversions.ConvertIdentifier(variable.Identifier.Identifier); + var expressionSyntax = (CSSyntax.ExpressionSyntax) variable.Expression.Accept(_triviaConvertingVisitor); + + CSSyntax.JoinIntoClauseSyntax joinIntoClauseSyntax = null; + if (js is VBSyntax.GroupJoinClauseSyntax gjs) { + joinIntoClauseSyntax = gjs.AggregationVariables + .Where(a => a.Aggregation is VBSyntax.GroupAggregationSyntax) + .Select(a => SyntaxFactory.JoinIntoClause(CommonConversions.ConvertIdentifier(a.NameEquals.Identifier.Identifier))) + .SingleOrDefault(); + } + return SyntaxFactory.JoinClause(null, convertIdentifier, expressionSyntax, joinLhs, joinRhs, joinIntoClauseSyntax); + } + + private CSSyntax.ExpressionSyntax SingleExpression(IReadOnlyCollection expressions) + { + if (expressions.Count == 1) return expressions.Single(); + return SyntaxFactory.AnonymousObjectCreationExpression(SyntaxFactory.SeparatedList(expressions.Select((e, i) => + SyntaxFactory.AnonymousObjectMemberDeclarator(SyntaxFactory.NameEquals($"key{i}"), e) + ))); + } + } +} \ No newline at end of file diff --git a/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs b/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs index ff8594847..a0d92c6b2 100644 --- a/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs +++ b/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs @@ -17,7 +17,6 @@ namespace ICSharpCode.CodeConverter.Shared { public class ProjectConversion { - private readonly string _solutionDir; private readonly Compilation _sourceCompilation; private readonly IEnumerable _syntaxTreesToConvert; // ReSharper disable once StaticMemberInGenericType - Stateless @@ -27,16 +26,10 @@ public class ProjectConversion private readonly ILanguageConversion _languageConversion; private readonly bool _handlePartialConversion; - private ProjectConversion(Compilation sourceCompilation, string solutionDir, ILanguageConversion languageConversion, Compilation convertedCompilation) - : this(sourceCompilation, sourceCompilation.SyntaxTrees.Where(t => t.FilePath.StartsWith(solutionDir)), languageConversion, convertedCompilation) - { - _solutionDir = solutionDir; - } - private ProjectConversion(Compilation sourceCompilation, IEnumerable syntaxTreesToConvert, ILanguageConversion languageConversion, Compilation convertedCompilation) { _languageConversion = languageConversion; - this._sourceCompilation = sourceCompilation; + _sourceCompilation = sourceCompilation; _syntaxTreesToConvert = syntaxTreesToConvert.ToList(); _handlePartialConversion = _syntaxTreesToConvert.Count() == 1; languageConversion.Initialize(convertedCompilation.RemoveAllSyntaxTrees()); @@ -79,10 +72,15 @@ public static async Task ConvertSingle(Compilation compilation public static IEnumerable ConvertProjectContents(Project project, ILanguageConversion languageConversion) { + var compilation = project.GetCompilationAsync(); var solutionFilePath = project.Solution.FilePath; var solutionDir = Path.GetDirectoryName(solutionFilePath); - var compilation = project.GetCompilationAsync().GetAwaiter().GetResult(); - var projectConversion = new ProjectConversion(compilation, solutionDir, languageConversion, GetConvertedCompilationWithProjectReferences(project, languageConversion)); + var projectOutputDir = Path.GetDirectoryName(project.OutputFilePath); + var guessAtProjectIntermediateOutputDir = projectOutputDir.Replace(@"\bin\", @"\obj\"); + var syntaxTrees = compilation.GetAwaiter().GetResult().SyntaxTrees.Where(t => + t.FilePath.StartsWith(solutionDir) && !t.FilePath.StartsWith(projectOutputDir) && + !t.FilePath.StartsWith(guessAtProjectIntermediateOutputDir)); + var projectConversion = new ProjectConversion(compilation.GetAwaiter().GetResult(), syntaxTrees, languageConversion, GetConvertedCompilationWithProjectReferences(project, languageConversion)); foreach (var conversionResult in ConvertProjectContents(projectConversion)) yield return conversionResult; } @@ -150,7 +148,7 @@ private void AddProjectWarnings() var nonFatalWarningsOrNull = _languageConversion.GetWarningsOrNull(); if (!string.IsNullOrWhiteSpace(nonFatalWarningsOrNull)) { - var warningsDescription = Path.Combine(_solutionDir ?? "", _sourceCompilation.AssemblyName, "ConversionWarnings.txt"); + var warningsDescription = Path.Combine("", _sourceCompilation.AssemblyName, "ConversionWarnings.txt"); _errors.TryAdd(warningsDescription, nonFatalWarningsOrNull); } } diff --git a/Tests/CSharp/ExpressionTests.cs b/Tests/CSharp/ExpressionTests.cs index f1d512ca4..03d6e997d 100644 --- a/Tests/CSharp/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests.cs @@ -897,6 +897,149 @@ from _lineItemCalculation in _claimComponentSummary.Last() }"); } + [Fact] + public void LinqPartitionDistinct() + { + TestConversionVisualBasicToCSharp(@"Private Shared Function FindPicFilePath() As IEnumerable(Of String) + Dim words = {""an"", ""apple"", ""a"", ""day"", ""keeps"", ""the"", ""doctor"", ""away""} + + Return From word In words + Skip 1 + Skip While word.Length >= 1 + Take While word.Length < 5 + Take 2 + Distinct +End Function", @"private static IEnumerable FindPicFilePath() +{ + var words = new[] { ""an"", ""apple"", ""a"", ""day"", ""keeps"", ""the"", ""doctor"", ""away"" }; + + return words +.Skip(1) +.SkipWhile(word => word.Length >= 1) +.TakeWhile(word => word.Length < 5) +.Take(2) +.Distinct(); +}"); + } + + [Fact] + public void LinqGroupByAnonymous() + { + //Very hard to automated test comments on such a complicated query + TestConversionVisualBasicToCSharpWithoutComments(@"Imports System.Runtime.CompilerServices + +Public Class AccountEntry + Public Property LookupAccountEntryTypeId As Object + Public Property LookupAccountEntrySourceId As Object + Public Property SponsorId As Object + Public Property LookupFundTypeId As Object + Public Property StartDate As Object + Public Property SatisfiedDate As Object + Public Property InterestStartDate As Object + Public Property ComputeInterestFlag As Object + Public Property SponsorClaimRevision As Object + Public Property Amount As Decimal + Public Property AccountTransactions As List(Of Object) + Public Property AccountEntryClaimDetails As List(Of AccountEntry) +End Class + +Module Ext + + Public Function Reduce(ByVal accountEntries As IEnumerable(Of AccountEntry)) As IEnumerable(Of AccountEntry) + Return ( + From _accountEntry In accountEntries + Where _accountEntry.Amount > 0D + Group By _keys = New With + { + Key .LookupAccountEntryTypeId = _accountEntry.LookupAccountEntryTypeId, + Key .LookupAccountEntrySourceId = _accountEntry.LookupAccountEntrySourceId, + Key .SponsorId = _accountEntry.SponsorId, + Key .LookupFundTypeId = _accountEntry.LookupFundTypeId, + Key .StartDate = _accountEntry.StartDate, + Key .SatisfiedDate = _accountEntry.SatisfiedDate, + Key .InterestStartDate = _accountEntry.InterestStartDate, + Key .ComputeInterestFlag = _accountEntry.ComputeInterestFlag, + Key .SponsorClaimRevision = _accountEntry.SponsorClaimRevision + } Into Group + Select New AccountEntry() With + { + .LookupAccountEntryTypeId = _keys.LookupAccountEntryTypeId, + .LookupAccountEntrySourceId = _keys.LookupAccountEntrySourceId, + .SponsorId = _keys.SponsorId, + .LookupFundTypeId = _keys.LookupFundTypeId, + .StartDate = _keys.StartDate, + .SatisfiedDate = _keys.SatisfiedDate, + .ComputeInterestFlag = _keys.ComputeInterestFlag, + .InterestStartDate = _keys.InterestStartDate, + .SponsorClaimRevision = _keys.SponsorClaimRevision, + .Amount = Group.Sum(Function(accountEntry) accountEntry.Amount), + .AccountTransactions = New List(Of Object)(), + .AccountEntryClaimDetails = + (From _accountEntry In Group From _claimDetail In _accountEntry.AccountEntryClaimDetails + Select _claimDetail).Reduce().ToList + } + ) + End Function +End Module", @"using System.Collections.Generic; +using System.Linq; + +public class AccountEntry +{ + public object LookupAccountEntryTypeId { get; set; } + public object LookupAccountEntrySourceId { get; set; } + public object SponsorId { get; set; } + public object LookupFundTypeId { get; set; } + public object StartDate { get; set; } + public object SatisfiedDate { get; set; } + public object InterestStartDate { get; set; } + public object ComputeInterestFlag { get; set; } + public object SponsorClaimRevision { get; set; } + public decimal Amount { get; set; } + public List AccountTransactions { get; set; } + public List AccountEntryClaimDetails { get; set; } +} + +static class Ext +{ + public static IEnumerable Reduce(this IEnumerable accountEntries) + { + return (from _accountEntry in accountEntries + where _accountEntry.Amount > 0M + group _accountEntry by new + { + LookupAccountEntryTypeId = _accountEntry.LookupAccountEntryTypeId, + LookupAccountEntrySourceId = _accountEntry.LookupAccountEntrySourceId, + SponsorId = _accountEntry.SponsorId, + LookupFundTypeId = _accountEntry.LookupFundTypeId, + StartDate = _accountEntry.StartDate, + SatisfiedDate = _accountEntry.SatisfiedDate, + InterestStartDate = _accountEntry.InterestStartDate, + ComputeInterestFlag = _accountEntry.ComputeInterestFlag, + SponsorClaimRevision = _accountEntry.SponsorClaimRevision + } into Group + let _keys = Group.Key + select new AccountEntry() + { + LookupAccountEntryTypeId = _keys.LookupAccountEntryTypeId, + LookupAccountEntrySourceId = _keys.LookupAccountEntrySourceId, + SponsorId = _keys.SponsorId, + LookupFundTypeId = _keys.LookupFundTypeId, + StartDate = _keys.StartDate, + SatisfiedDate = _keys.SatisfiedDate, + ComputeInterestFlag = _keys.ComputeInterestFlag, + InterestStartDate = _keys.InterestStartDate, + SponsorClaimRevision = _keys.SponsorClaimRevision, + Amount = Group.Sum(accountEntry => accountEntry.Amount), + AccountTransactions = new List(), + AccountEntryClaimDetails = (from _accountEntry in Group + from _claimDetail in _accountEntry.AccountEntryClaimDetails + select _claimDetail).Reduce().ToList() + } +); + } +}"); + } + [Fact] public void PartiallyQualifiedName() { diff --git a/Tests/ProjectConverterTestBase.cs b/Tests/ProjectConverterTestBase.cs index ac9c6b9ee..c6044b506 100644 --- a/Tests/ProjectConverterTestBase.cs +++ b/Tests/ProjectConverterTestBase.cs @@ -72,7 +72,7 @@ private static void AssertAllConvertedFilesWereExpected(FileInfo[] expectedFiles Dictionary conversionResults, DirectoryInfo expectedResultDirectory, string originalSolutionDir) { - AssertSubset(expectedFiles.Select(f => f.FullName), conversionResults.Select(r => r.Key.Replace(originalSolutionDir, expectedResultDirectory.FullName))); + AssertSubset(expectedFiles.Select(f => f.FullName.Replace(expectedResultDirectory.FullName, "")), conversionResults.Select(r => r.Key.Replace(originalSolutionDir, ""))); } private void AssertAllExpectedFilesAreEqual(FileInfo[] expectedFiles, Dictionary conversionResults, @@ -92,9 +92,11 @@ private static void AssertNoConversionErrors(Dictionary expectedFiles, IEnumerable collection) + private static void AssertSubset(IEnumerable superset, IEnumerable subset) { - Assert.Subset(new HashSet(expectedFiles, StringComparer.OrdinalIgnoreCase), new HashSet(collection, StringComparer.OrdinalIgnoreCase)); + var notExpected = new HashSet(subset, StringComparer.OrdinalIgnoreCase); + notExpected.ExceptWith(new HashSet(superset, StringComparer.OrdinalIgnoreCase)); + Assert.Empty(notExpected); } private void AssertFileEqual(Dictionary conversionResults,