diff --git a/ICSharpCode.CodeConverter/Shared/AnnotationConstants.cs b/ICSharpCode.CodeConverter/Shared/AnnotationConstants.cs index f76a70c73..1981e4d66 100644 --- a/ICSharpCode.CodeConverter/Shared/AnnotationConstants.cs +++ b/ICSharpCode.CodeConverter/Shared/AnnotationConstants.cs @@ -1,9 +1,23 @@ -namespace ICSharpCode.CodeConverter.Shared +using Microsoft.CodeAnalysis; + +namespace ICSharpCode.CodeConverter.Shared { internal class AnnotationConstants { public const string SelectedNodeAnnotationKind = "CodeConverter.SelectedNode"; public const string AnnotatedNodeIsParentData = "CodeConverter.SelectedNode.IsAllChildrenOfThisNode"; public const string ConversionErrorAnnotationKind = "CodeConverter.ConversionError"; + public const string SourceStartLineAnnotationKind = "CodeConverter.SourceStartLine"; + public const string SourceEndLineAnnotationKind = "CodeConverter.SourceEndLine"; + + public static SyntaxAnnotation SourceStartLine(FileLinePositionSpan origLinespan) + { + return new SyntaxAnnotation(SourceStartLineAnnotationKind, origLinespan.StartLinePosition.Line.ToString()); + } + + public static SyntaxAnnotation SourceEndLine(FileLinePositionSpan origLinespan) + { + return new SyntaxAnnotation(SourceEndLineAnnotationKind, origLinespan.EndLinePosition.Line.ToString()); + } } } \ No newline at end of file diff --git a/ICSharpCode.CodeConverter/Shared/LineTriviaMapper.cs b/ICSharpCode.CodeConverter/Shared/LineTriviaMapper.cs new file mode 100644 index 000000000..0c14a6bb5 --- /dev/null +++ b/ICSharpCode.CodeConverter/Shared/LineTriviaMapper.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ICSharpCode.CodeConverter.Util; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace ICSharpCode.CodeConverter.Shared +{ + internal class LineTriviaMapper + { + private SyntaxNode _target; + private readonly SyntaxNode _source; + private readonly TextLineCollection _sourceLines; + private readonly IReadOnlyDictionary _targetLeadingTextLineFromSourceLine; + private readonly IReadOnlyDictionary _targetTrailingTextLineFromSourceLine; + private readonly List _leadingTriviaCarriedOver = new List(); + private readonly List _trailingTriviaCarriedOver = new List(); + private readonly Dictionary> Leading, List> Trailing)> _targetTokenToTrivia = new Dictionary>, List>)>(); + + public LineTriviaMapper(SyntaxNode source, TextLineCollection sourceLines, SyntaxNode target, Dictionary targetLeadingTextLineFromSourceLine, Dictionary targetTrailingTextLineFromSourceLine) + { + _source = source; + _sourceLines = sourceLines; + _target = target; + _targetLeadingTextLineFromSourceLine = targetLeadingTextLineFromSourceLine; + _targetTrailingTextLineFromSourceLine = targetTrailingTextLineFromSourceLine; + } + + /// + /// For each source line: + /// * Add leading trivia to the start of the first target line containing a node converted from that source line + /// * Add trailing trivia to the end of the last target line containing a node converted from that source line + /// Makes no attempt to convert whitespace/newline-only trivia + /// Currently doesn't deal with any within-line trivia (i.e. /* block comments */) + /// + public static SyntaxNode MapSourceTriviaToTarget(TSource source, TTarget target) + where TSource : SyntaxNode, ICompilationUnitSyntax where TTarget : SyntaxNode, ICompilationUnitSyntax + { + var originalTargetLines = target.GetText().Lines; + + var targetNodesBySourceStartLine = target.GetAnnotatedNodesAndTokens(AnnotationConstants.SourceStartLineAnnotationKind) + .ToLookup(n => n.GetAnnotations(AnnotationConstants.SourceStartLineAnnotationKind).Select(a => int.Parse(a.Data)).Min()) + .ToDictionary(g => g.Key, g => originalTargetLines.GetLineFromPosition(g.Min(x => x.Span.Start))); + + var targetNodesBySourceEndLine = target.GetAnnotatedNodesAndTokens(AnnotationConstants.SourceEndLineAnnotationKind) + .ToLookup(n => n.GetAnnotations(AnnotationConstants.SourceEndLineAnnotationKind).Select(a => int.Parse(a.Data)).Max()) + .ToDictionary(g => g.Key, g => originalTargetLines.GetLineFromPosition(g.Max(x => x.Span.End))); + + var sourceLines = source.GetText().Lines; + var lineTriviaMapper = new LineTriviaMapper(source, sourceLines, target, targetNodesBySourceStartLine, targetNodesBySourceEndLine); + + return lineTriviaMapper.GetTargetWithSourceTrivia(); + } + + /// + /// Possible future improvements: + /// * Performance: Probably faster to find tokens starting from position of last replaced token rather than from the root node each time + /// + private SyntaxNode GetTargetWithSourceTrivia() + { + // Reverse iterate to ensure trivia never ends up after the place it came from (consider #if directive or opening brace of method) + for (int i = _sourceLines.Count - 1; i >= 0; i--) { + MapLeading(i); + MapTrailing(i); + } + + //Reverse trivia due to above reverse looping + foreach (var trivia in _targetTokenToTrivia) { + trivia.Value.Leading.Reverse(); + trivia.Value.Trailing.Reverse(); + } + + BalanceTrivia(); + + return _target.ReplaceTokens(_targetTokenToTrivia.Keys, AttachMappedTrivia); + } + + /// + /// Trailing trivia can't contain multiple newlines (it gets lost during formatting), so prepend as leading trivia of the next token + /// + private void BalanceTrivia() + { + foreach (var trivia in _targetTokenToTrivia.Where(t => t.Value.Trailing.Count > 1).ToList()) { + var lastIndexToKeep = trivia.Value.Trailing.FindIndex(tl => tl.Any(t => t.IsEndOfLine())); + var moveToLeadingTrivia = trivia.Value.Trailing.Skip(lastIndexToKeep + 1).ToList(); + if (moveToLeadingTrivia.Any()) { + var nextTrivia = GetTargetTriviaCollection(trivia.Key.GetNextToken(true)); + nextTrivia.Leading.InsertRange(0, moveToLeadingTrivia); + trivia.Value.Trailing.RemoveRange(lastIndexToKeep + 1, moveToLeadingTrivia.Count); + } + } + } + + private SyntaxToken AttachMappedTrivia(SyntaxToken original, SyntaxToken rewritten) + { + var trivia = _targetTokenToTrivia[original]; + return rewritten.WithLeadingTrivia(trivia.Leading.SelectMany(tl => tl)) + .WithTrailingTrivia(trivia.Trailing.SelectMany(tl => tl)); + } + + private void MapTrailing(int sourceLineIndex) + { + var sourceLine = _sourceLines[sourceLineIndex]; + var endOfSourceLine = sourceLine.FindLastTokenWithinLine(_source); + + if (endOfSourceLine.TrailingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) { + _trailingTriviaCarriedOver.Add(endOfSourceLine.TrailingTrivia); + } + + if (_trailingTriviaCarriedOver.Any()) { + var targetLine = GetTargetLine(_targetTrailingTextLineFromSourceLine, sourceLineIndex); + if (targetLine == default) targetLine = GetTargetLine(_targetLeadingTextLineFromSourceLine, sourceLineIndex); + if (targetLine != default) { + var originalToReplace = targetLine.GetTrailingForLine(_target); + if (originalToReplace != null) { + var targetTrivia = GetTargetTriviaCollection(originalToReplace); + targetTrivia.Trailing.AddRange(_trailingTriviaCarriedOver.Select(t => t.ConvertTrivia().ToList())); + _trailingTriviaCarriedOver.Clear(); + } + } + } + } + + private void MapLeading(int sourceLineIndex) + { + var sourceLine = _sourceLines[sourceLineIndex]; + var startOfSourceLine = sourceLine.FindFirstTokenWithinLine(_source); + + if (startOfSourceLine.LeadingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) { + _leadingTriviaCarriedOver.Add(startOfSourceLine.LeadingTrivia); + } + + if (_leadingTriviaCarriedOver.Any()) { + var targetLine = GetTargetLine(_targetLeadingTextLineFromSourceLine, sourceLineIndex); + if (targetLine == default) targetLine = GetTargetLine(_targetTrailingTextLineFromSourceLine, sourceLineIndex); + if (targetLine != default) { + var originalToReplace = targetLine.GetLeadingForLine(_target); + if (originalToReplace != default) { + var targetTrivia = GetTargetTriviaCollection(originalToReplace); + targetTrivia.Leading.AddRange(_leadingTriviaCarriedOver.Select(t => t.ConvertTrivia().ToList())); + _leadingTriviaCarriedOver.Clear(); + return; + } + } + } + } + + private TextLine GetTargetLine(IReadOnlyDictionary sourceToTargetLine, int sourceLineIndex) + { + if (sourceToTargetLine.TryGetValue(sourceLineIndex, out var targetLine)) return targetLine; + return default; + } + + private (List> Leading, List> Trailing) GetTargetTriviaCollection(SyntaxToken toReplace) + { + if (!_targetTokenToTrivia.TryGetValue(toReplace, out var targetTrivia)) { + targetTrivia = (new List>(), new List>()); + _targetTokenToTrivia[toReplace] = targetTrivia; + } + + return targetTrivia; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs b/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs index d65b20d77..3a41ef085 100644 --- a/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs +++ b/ICSharpCode.CodeConverter/Shared/ProjectConversion.cs @@ -188,7 +188,11 @@ private static async Task> ConvertProjectContents( Document document = await _languageConversion.SingleSecondPass(convertedDocument); if (_returnSelectedNode) { selectedNode = await GetSelectedNode(document); + var extraLeadingTrivia = selectedNode.GetFirstToken().GetPreviousToken().TrailingTrivia; + var extraTrailingTrivia = selectedNode.GetLastToken().GetNextToken().LeadingTrivia; selectedNode = Formatter.Format(selectedNode, document.Project.Solution.Workspace); + if (extraLeadingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) selectedNode = selectedNode.WithPrependedLeadingTrivia(extraLeadingTrivia); + if (extraTrailingTrivia.Any(t => !t.IsWhitespaceOrEndOfLine())) selectedNode = selectedNode.WithAppendedTrailingTrivia(extraTrailingTrivia); } else { selectedNode = await document.GetSyntaxRootAsync(); selectedNode = Formatter.Format(selectedNode, document.Project.Solution.Workspace); diff --git a/ICSharpCode.CodeConverter/Shared/TextLineExtensions.cs b/ICSharpCode.CodeConverter/Shared/TextLineExtensions.cs new file mode 100644 index 000000000..98691a240 --- /dev/null +++ b/ICSharpCode.CodeConverter/Shared/TextLineExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ICSharpCode.CodeConverter.Util; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace ICSharpCode.CodeConverter.Shared +{ + + internal static class TextLineExtensions + { + public static bool ContainsPosition(this TextLine line, int position) + { + return line.Start <= position && position <= line.End; + } + + public static SyntaxToken FindFirstTokenWithinLine(this TextLine line, SyntaxNode node) + { + var syntaxToken = node.FindToken(line.Start); + var previousToken = syntaxToken.GetPreviousToken(); + var nextToken = syntaxToken.GetNextToken(); + return new[] { previousToken, syntaxToken, nextToken } + .FirstOrDefault(t => line.ContainsPosition(t.Span.Start)); + } + + public static SyntaxToken FindLastTokenWithinLine(this TextLine line, SyntaxNode node) + { + var syntaxToken = node.FindToken(line.End); + var previousToken = syntaxToken.GetPreviousToken(); + var nextToken = syntaxToken.GetNextToken(); + return new[] { nextToken, syntaxToken, previousToken } + .FirstOrDefault(t => line.ContainsPosition(t.Span.End) && t.Width() > 0); + } + + public static SyntaxToken GetLeadingForLine(this TextLine targetLine, SyntaxNode target) + { + var toReplace = target.FindNonZeroWidthToken(targetLine.Start); + if (toReplace.Span.End < targetLine.Start) { + toReplace = toReplace.GetNextToken(); //TODO: Find out why FindToken is off by one from what I want sometimes, is there a better alternative? + } + + return toReplace; + } + + public static SyntaxToken GetTrailingForLine(this TextLine targetLine, SyntaxNode target) + { + var toReplace = target.FindNonZeroWidthToken(targetLine.End); + if (toReplace.Width() == 0) { + toReplace = toReplace.GetPreviousToken(); //Never append *trailing* trivia to the end of file token + } + + return toReplace; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.CodeConverter/Shared/TriviaMapping.cs b/ICSharpCode.CodeConverter/Shared/TriviaMapping.cs new file mode 100644 index 000000000..8d0901e55 --- /dev/null +++ b/ICSharpCode.CodeConverter/Shared/TriviaMapping.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ICSharpCode.CodeConverter.Util; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace ICSharpCode.CodeConverter.Shared +{ + + internal struct TriviaMapping + { + public int SourceLine; + public int TargetLine; + public SyntaxTriviaList SourceTrivia; + public SyntaxToken TargetToken; + public bool IsLeading; + + public TriviaMapping(int sourceLine, int targetLine, SyntaxTriviaList sourceTrivia, SyntaxToken targetToken, bool isLeading) + { + SourceLine = sourceLine; + TargetLine = targetLine; + SourceTrivia = sourceTrivia; + TargetToken = targetToken; + IsLeading = isLeading; + } + + public override bool Equals(object obj) + { + return obj is TriviaMapping other && + SourceLine == other.SourceLine && + TargetLine == other.TargetLine && + SourceTrivia.Equals(other.SourceTrivia) && + TargetToken.Equals(other.TargetToken) && + IsLeading == other.IsLeading; + } + + public override int GetHashCode() + { + var hashCode = -1113933263; + hashCode = hashCode * -1521134295 + SourceLine.GetHashCode(); + hashCode = hashCode * -1521134295 + TargetLine.GetHashCode(); + hashCode = hashCode * -1521134295 + SourceTrivia.GetHashCode(); + hashCode = hashCode * -1521134295 + TargetToken.GetHashCode(); + hashCode = hashCode * -1521134295 + IsLeading.GetHashCode(); + return hashCode; + } + + public void Deconstruct(out int sourceLine, out int targetLine, out SyntaxTriviaList sourceTrivia, out SyntaxToken targetToken, out bool isLeading) + { + sourceLine = SourceLine; + targetLine = TargetLine; + sourceTrivia = SourceTrivia; + targetToken = TargetToken; + isLeading = IsLeading; + } + + public static implicit operator (int SourceLine, int TargetLine, SyntaxTriviaList SourceTrivia, SyntaxToken TargetToken, bool IsLeading)(TriviaMapping value) + { + return (value.SourceLine, value.TargetLine, value.SourceTrivia, value.TargetToken, value.IsLeading); + } + + public static implicit operator TriviaMapping((int SourceLine, int TargetLine, SyntaxTriviaList SourceTrivia, SyntaxToken TargetToken, bool IsLeading) value) + { + return new TriviaMapping(value.SourceLine, value.TargetLine, value.SourceTrivia, value.TargetToken, value.IsLeading); + } + + public override string ToString() + { + var type = IsLeading ? "Leading" : "Trailing"; + return $"{type} {TargetLine}: {TargetToken} - {SourceTrivia}"; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.CodeConverter/Util/SyntaxNodeExtensions.cs b/ICSharpCode.CodeConverter/Util/SyntaxNodeExtensions.cs index a7bd2beb4..7959169bc 100644 --- a/ICSharpCode.CodeConverter/Util/SyntaxNodeExtensions.cs +++ b/ICSharpCode.CodeConverter/Util/SyntaxNodeExtensions.cs @@ -9,10 +9,14 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using CS = Microsoft.CodeAnalysis.CSharp; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.VisualBasic; using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using VBasic = Microsoft.CodeAnalysis.VisualBasic; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; using AnonymousObjectCreationExpressionSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.AnonymousObjectCreationExpressionSyntax; using ArgumentListSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax; using ArrayRankSpecifierSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.ArrayRankSpecifierSyntax; @@ -40,6 +44,7 @@ using TypeSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax; using UsingStatementSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.UsingStatementSyntax; using WhileStatementSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.WhileStatementSyntax; +using VBCommonConversions = ICSharpCode.CodeConverter.VB.CommonConversions; namespace ICSharpCode.CodeConverter.Util { @@ -211,12 +216,41 @@ public static TSyntaxNode FindInnermostCommonNode(this IEnumerable< public static ISymbol GetEnclosingDeclaredTypeSymbol(this SyntaxNode node, SemanticModel semanticModel) { - var typeBlockSyntax = (SyntaxNode) node.GetAncestor() + var typeBlockSyntax = (SyntaxNode)node.GetAncestor() ?? node.GetAncestor(); if (typeBlockSyntax == null) return null; return semanticModel.GetDeclaredSymbol(typeBlockSyntax); } + public static SyntaxList WithSourceMappingFrom(this SyntaxList converted, SyntaxNode node) where T : SyntaxNode + { + if (!converted.Any()) return converted; + var origLinespan = node.SyntaxTree.GetLineSpan(node.Span); + var first = converted.First(); + converted = converted.Replace(first, node.CopyAnnotationsTo(first).WithSourceStartLineAnnotation(origLinespan)); + var last = converted.Last(); + return converted.Replace(last, last.WithSourceEndLineAnnotation(origLinespan)); + } + + public static T WithSourceMappingFrom(this T converted, SyntaxNodeOrToken fromSource) where T : SyntaxNode + { + if (converted == null) return null; + var startLinespan = fromSource.SyntaxTree.GetLineSpan(fromSource.Span); + return converted + .WithSourceStartLineAnnotation(startLinespan) + .WithSourceEndLineAnnotation(startLinespan); + } + + public static T WithSourceStartLineAnnotation(this T node, FileLinePositionSpan sourcePosition) where T : SyntaxNode + { + return node.WithAdditionalAnnotations(AnnotationConstants.SourceStartLine(sourcePosition)); + } + + public static T WithSourceEndLineAnnotation(this T node, FileLinePositionSpan sourcePosition) where T : SyntaxNode + { + return node.WithAdditionalAnnotations(AnnotationConstants.SourceEndLine(sourcePosition)); + } + /// /// create a new root node from the given root after adding annotations to the tokens /// @@ -755,6 +789,12 @@ public static SyntaxToken WithConvertedTriviaFrom(this SyntaxToken node, SyntaxN return node.WithConvertedLeadingTriviaFrom(otherNode).WithConvertedTrailingTriviaFrom(otherNode); } + public static T WithConvertedLeadingTriviaFrom(this T node, SyntaxToken fromToken) where T : SyntaxNode + { + var firstConvertedToken = node.GetFirstToken(); + return node.ReplaceToken(firstConvertedToken, firstConvertedToken.WithConvertedLeadingTriviaFrom(fromToken)); + } + public static SyntaxToken WithConvertedLeadingTriviaFrom(this SyntaxToken node, SyntaxNode otherNode) { var firstToken = otherNode?.GetFirstToken(); @@ -768,6 +808,12 @@ public static SyntaxToken WithConvertedLeadingTriviaFrom(this SyntaxToken node, return node.WithLeadingTrivia(convertedTrivia); } + public static T WithConvertedTrailingTriviaFrom(this T node, SyntaxToken fromToken) where T : SyntaxNode + { + var lastConvertedToken = node.GetLastToken(); + return node.ReplaceToken(lastConvertedToken, lastConvertedToken.WithConvertedTrailingTriviaFrom(fromToken)); + } + public static SyntaxToken WithConvertedTrailingTriviaFrom(this SyntaxToken node, SyntaxNode otherNode) { return node.WithConvertedTrailingTriviaFrom(otherNode?.GetLastToken()); @@ -782,13 +828,7 @@ public static SyntaxToken WithConvertedTrailingTriviaFrom(this SyntaxToken node, public static IEnumerable ImportantTrailingTrivia(this SyntaxToken node) { - return node.TrailingTrivia.Where(x => !IsWhitespaceTrivia(x) - ); - } - - public static bool IsWhitespaceTrivia(this SyntaxTrivia trivia) - { - return trivia.IsKind(CSSyntaxKind.WhitespaceTrivia) || trivia.IsKind(CSSyntaxKind.EndOfLineTrivia) || trivia.IsKind(CSSyntaxKind.WhitespaceTrivia) || trivia.IsKind(CSSyntaxKind.EndOfLineTrivia); + return node.TrailingTrivia.Where(x => !x.IsWhitespaceOrEndOfLine()); } public static bool ParentHasSameTrailingTrivia(this SyntaxNode otherNode) @@ -798,7 +838,13 @@ public static bool ParentHasSameTrailingTrivia(this SyntaxNode otherNode) public static IEnumerable ConvertTrivia(this IReadOnlyCollection triviaToConvert) { - return triviaToConvert.Select(t => t.Language == "Visual Basic" ? ConvertVBTrivia(t) : ConvertCSTrivia(t)).Where(x => x != default(SyntaxTrivia)); + return triviaToConvert.SelectMany(t => { + if (t.Language == LanguageNames.VisualBasic) { + return ConvertVBTrivia(t).Yield(); + } else { + return ConvertCSTrivia(t); + } + }).Where(x => x != default(SyntaxTrivia)); } private static SyntaxTrivia ConvertVBTrivia(SyntaxTrivia t) @@ -830,36 +876,88 @@ private static SyntaxTrivia ConvertVBTrivia(SyntaxTrivia t) : default(SyntaxTrivia); } - private static SyntaxTrivia ConvertCSTrivia(SyntaxTrivia t) + private static IEnumerable ConvertCSTrivia(SyntaxTrivia t) { - if (t.IsKind(CSSyntaxKind.SingleLineCommentTrivia)) - return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.CommentTrivia, $"' {t.GetCommentText()}"); + var endOfLine = SyntaxTriviaExtensions.GetEndOfLine(LanguageNames.VisualBasic); + + if (t.IsKind(CSSyntaxKind.SingleLineCommentTrivia)) { + yield return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.CommentTrivia, $"' {t.GetCommentText()}"); + yield break; + } if (t.IsKind(CSSyntaxKind.SingleLineDocumentationCommentTrivia)) { - var previousWhitespace = t.GetPreviousTrivia(t.SyntaxTree, CancellationToken.None).ToString(); + var previousTrivia = t.GetPreviousTrivia(t.SyntaxTree, CancellationToken.None); + var previousWhitespace = previousTrivia.IsWhitespace() ? previousTrivia.ToString() : ""; var commentTextLines = t.GetCommentText().Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); var multiLine = commentTextLines.Length > 1; var outputCommentText = multiLine ? "''' " + String.Join($"\r\n{previousWhitespace}''' ", commentTextLines) : $"' {commentTextLines.Single()}"; - return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.DocumentationCommentTrivia, - outputCommentText); + yield return VBSyntaxFactory.CommentTrivia(outputCommentText); + yield return endOfLine; + yield break; } if (t.IsKind(CSSyntaxKind.WhitespaceTrivia)) { - return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.WhitespaceTrivia, t.ToString()); + yield return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.WhitespaceTrivia, t.ToString()); + yield break; } if (t.IsKind(CSSyntaxKind.EndOfLineTrivia)) { - // Mapping one to one here leads to newlines appearing where the natural line-end was in VB. - // e.g. ToString\r\n() - // Because C Sharp needs those brackets. Handling each possible case of this is far more effort than it's worth. - return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.EndOfLineTrivia, t.ToString()); + yield return VBSyntaxFactory.SyntaxTrivia(VBSyntaxKind.EndOfLineTrivia, t.ToString()); + yield break; } - var convertedKind = t.GetVBKind(); - return convertedKind.HasValue - ? VBSyntaxFactory.CommentTrivia($"' TODO ERROR: Skipped {convertedKind.Value}") - : default(SyntaxTrivia); + if (t.IsKind(CSSyntaxKind.DisabledTextTrivia)) { + //TODO Actually use converter + yield return VBSyntaxFactory.DisabledTextTrivia("' Skipped during conversion: " + t.ToString().Trim('\r', '\n').Replace("\n", "\n'")); + yield return endOfLine; + yield break; + } + + var structured = GetStructuredTrivia(t); + if (structured != null) { + yield return VBSyntaxFactory.Trivia(structured); + yield return endOfLine; + yield break; + } + + yield return VBSyntaxFactory.CommentTrivia($"' TODO ERROR: Skipped {t}\r\n"); + } + + private static VBSyntax.StructuredTriviaSyntax GetStructuredTrivia(SyntaxTrivia t) + { + + if (t.IsKind(CSSyntaxKind.RegionDirectiveTrivia)) { + var structure = ((CSSyntax.RegionDirectiveTriviaSyntax)t.GetStructure()); + string name = structure.EndOfDirectiveToken.LeadingTrivia.Single().ToString(); + var regionSyntax = VBSyntaxFactory.RegionDirectiveTrivia(VBSyntaxFactory.Token(VBSyntaxKind.HashToken), VBSyntaxFactory.Token(VBSyntaxKind.RegionKeyword), VBSyntaxFactory.StringLiteralToken("\"" + name + "\"", name)); + return regionSyntax; + } + + if (t.IsKind(CSSyntaxKind.EndRegionDirectiveTrivia)) { + var regionSyntax = VBSyntaxFactory.EndRegionDirectiveTrivia(); + return regionSyntax; + } + + if (t.IsKind(CSSyntaxKind.IfDirectiveTrivia) && t.GetStructure() is CSSyntax.IfDirectiveTriviaSyntax idts) { + //TODO Actually use converter + return VBSyntaxFactory.IfDirectiveTrivia(VBasic.SyntaxFactory.Token(VBSyntaxKind.IfKeyword), VBasic.SyntaxFactory.ParseExpression(idts.Condition.ToString())); + } + + if (t.IsKind(CSSyntaxKind.ElifDirectiveTrivia) && t.GetStructure() is CSSyntax.IfDirectiveTriviaSyntax eidts) { + //TODO Actually use converter + return VBSyntaxFactory.IfDirectiveTrivia(VBasic.SyntaxFactory.Token(VBSyntaxKind.IfKeyword), VBasic.SyntaxFactory.ParseExpression(eidts.Condition.ToString())); + } + + if (t.IsKind(CSSyntaxKind.ElseDirectiveTrivia)) { + return VBSyntaxFactory.ElseDirectiveTrivia(); + } + + if (t.IsKind(CSSyntaxKind.EndIfDirectiveTrivia)) { + return VBSyntaxFactory.EndIfDirectiveTrivia(); + } + + return null; } public static T WithoutTrailingEndOfLineTrivia(this T cSharpNode) where T : CSharpSyntaxNode @@ -1534,7 +1632,7 @@ private static string Truncate(this string input, int maxLength = 30, string tru public static T WithCsTrailingErrorComment(this T dummyDestNode, VisualBasicSyntaxNode sourceNode, - Exception exception) where T: CSharpSyntaxNode + Exception exception) where T : CSharpSyntaxNode { var errorDirective = SyntaxFactory.ParseTrailingTrivia($"#error Cannot convert {sourceNode.GetType().Name} - see comment for details{Environment.NewLine}"); var errorDescription = sourceNode.DescribeConversionError(exception); @@ -1574,5 +1672,15 @@ public static bool ContainsDeclaredVisibility(this SyntaxTokenList modifiers, bo { return modifiers.Any(m => m.IsCsVisibility(isVariableOrConst, isConstructor)); } + + public static SyntaxToken FindNonZeroWidthToken(this SyntaxNode node, int position) + { + var syntaxToken = node.FindToken(position); + if (syntaxToken.FullWidth() == 0) { + return syntaxToken.GetPreviousToken(); + } else { + return syntaxToken; + } + } } -} +} \ No newline at end of file diff --git a/ICSharpCode.CodeConverter/Util/SyntaxTokenExtensions.cs b/ICSharpCode.CodeConverter/Util/SyntaxTokenExtensions.cs index ad15fd4b2..e69c24cc8 100644 --- a/ICSharpCode.CodeConverter/Util/SyntaxTokenExtensions.cs +++ b/ICSharpCode.CodeConverter/Util/SyntaxTokenExtensions.cs @@ -10,6 +10,7 @@ using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; using VisualBasicExtensions = Microsoft.CodeAnalysis.VisualBasic.VisualBasicExtensions; using VBasic = Microsoft.CodeAnalysis.VisualBasic; +using ICSharpCode.CodeConverter.Shared; namespace ICSharpCode.CodeConverter.Util { @@ -768,12 +769,12 @@ public static bool IsWord(this SyntaxToken token) || SyntaxFacts.IsPreprocessorKeyword(token.Kind()); } - public static SyntaxToken GetNextNonZeroWidthTokenOrEndOfFile(this SyntaxToken token) + public static SyntaxToken GetNextNonZeroWidthCsTokenOrEndOfFile(this SyntaxToken token) { - return token.GetNextTokenOrEndOfFile(); + return token.GetNextCsTokenOrEndOfFile(); } - public static SyntaxToken GetNextTokenOrEndOfFile( + public static SyntaxToken GetNextCsTokenOrEndOfFile( this SyntaxToken token, bool includeZeroWidth = false, bool includeSkipped = false, @@ -896,7 +897,7 @@ public static IEnumerable GetAllTrailingTrivia(this SyntaxToken to yield return trivia; } - var nextToken = token.GetNextTokenOrEndOfFile(includeZeroWidth: true, includeSkipped: true, includeDirectives: true, includeDocumentationComments: true); + var nextToken = token.GetNextCsTokenOrEndOfFile(includeZeroWidth: true, includeSkipped: true, includeDirectives: true, includeDocumentationComments: true); foreach (var trivia in nextToken.LeadingTrivia) { yield return trivia; @@ -1059,5 +1060,23 @@ public static bool IsCsVisibility(this SyntaxToken token, bool isVariableOrConst || isVariableOrConst && token.IsKind(SyntaxKind.ConstKeyword) || isConstructor && token.IsKind(SyntaxKind.StaticKeyword); } + + public static SyntaxToken WithSourceMappingFrom(this SyntaxToken converted, SyntaxToken fromToken) + { + var origLinespan = fromToken.SyntaxTree.GetLineSpan(fromToken.Span); + return fromToken.CopyAnnotationsTo(converted) + .WithSourceStartLineAnnotation(origLinespan) + .WithSourceEndLineAnnotation(origLinespan); + } + + public static SyntaxToken WithSourceStartLineAnnotation(this SyntaxToken node, FileLinePositionSpan sourcePosition) + { + return node.WithAdditionalAnnotations(AnnotationConstants.SourceStartLine(sourcePosition)); + } + + public static SyntaxToken WithSourceEndLineAnnotation(this SyntaxToken node, FileLinePositionSpan sourcePosition) + { + return node.WithAdditionalAnnotations(AnnotationConstants.SourceEndLine(sourcePosition)); + } } } diff --git a/ICSharpCode.CodeConverter/Util/SyntaxTriviaExtensions.cs b/ICSharpCode.CodeConverter/Util/SyntaxTriviaExtensions.cs index 3b445be8b..5dc5e4788 100644 --- a/ICSharpCode.CodeConverter/Util/SyntaxTriviaExtensions.cs +++ b/ICSharpCode.CodeConverter/Util/SyntaxTriviaExtensions.cs @@ -4,10 +4,11 @@ using System.Text; using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using CSharp = Microsoft.CodeAnalysis.CSharp; +using static Microsoft.CodeAnalysis.CSharp.CSharpExtensions; +using CS = Microsoft.CodeAnalysis.CSharp; using VBasic = Microsoft.CodeAnalysis.VisualBasic; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; namespace ICSharpCode.CodeConverter.Util { @@ -33,37 +34,37 @@ internal static class SyntaxTriviaExtensions /// private static IEnumerable GetSkippedTokens(SyntaxTriviaList list) { - return list.Where(trivia => trivia.RawKind == (int)SyntaxKind.SkippedTokensTrivia) - .SelectMany(t => ((SkippedTokensTriviaSyntax)t.GetStructure()).Tokens); + return list.Where(trivia => trivia.RawKind == (int)CS.SyntaxKind.SkippedTokensTrivia) + .SelectMany(t => ((CSSyntax.SkippedTokensTriviaSyntax)t.GetStructure()).Tokens); } - private static readonly Dictionary VBToCSSyntaxKinds = new Dictionary { - {VBasic.SyntaxKind.SkippedTokensTrivia, SyntaxKind.SkippedTokensTrivia}, - {VBasic.SyntaxKind.DisabledTextTrivia, SyntaxKind.DisabledTextTrivia}, - {VBasic.SyntaxKind.ConstDirectiveTrivia, SyntaxKind.DefineDirectiveTrivia}, // Just a guess - {VBasic.SyntaxKind.IfDirectiveTrivia, SyntaxKind.IfDirectiveTrivia}, - {VBasic.SyntaxKind.ElseIfDirectiveTrivia, SyntaxKind.ElifDirectiveTrivia}, - {VBasic.SyntaxKind.ElseDirectiveTrivia, SyntaxKind.ElseDirectiveTrivia}, - {VBasic.SyntaxKind.EndIfDirectiveTrivia, SyntaxKind.EndIfDirectiveTrivia}, - //{VBSyntaxKind.RegionDirectiveTrivia, SyntaxKind.RegionDirectiveTrivia}, Oh no I accidentally disabled regions :O ;) - //{VBSyntaxKind.EndRegionDirectiveTrivia, SyntaxKind.EndRegionDirectiveTrivia}, - {VBasic.SyntaxKind.EnableWarningDirectiveTrivia, SyntaxKind.WarningDirectiveTrivia}, - {VBasic.SyntaxKind.DisableWarningDirectiveTrivia, SyntaxKind.WarningDirectiveTrivia}, - {VBasic.SyntaxKind.ReferenceDirectiveTrivia, SyntaxKind.ReferenceDirectiveTrivia}, - {VBasic.SyntaxKind.BadDirectiveTrivia, SyntaxKind.BadDirectiveTrivia}, - {VBasic.SyntaxKind.ConflictMarkerTrivia, SyntaxKind.ConflictMarkerTrivia}, - {VBasic.SyntaxKind.ExternalSourceDirectiveTrivia, SyntaxKind.LoadDirectiveTrivia}, //Just a guess - {VBasic.SyntaxKind.ExternalChecksumDirectiveTrivia, SyntaxKind.LineDirectiveTrivia}, // Even more random guess + private static readonly Dictionary VBToCSSyntaxKinds = new Dictionary { + {VBasic.SyntaxKind.SkippedTokensTrivia, CS.SyntaxKind.SkippedTokensTrivia}, + {VBasic.SyntaxKind.DisabledTextTrivia, CS.SyntaxKind.DisabledTextTrivia}, + {VBasic.SyntaxKind.ConstDirectiveTrivia, CS.SyntaxKind.DefineDirectiveTrivia}, // Just a guess + {VBasic.SyntaxKind.IfDirectiveTrivia, CS.SyntaxKind.IfDirectiveTrivia}, + {VBasic.SyntaxKind.ElseIfDirectiveTrivia, CS.SyntaxKind.ElifDirectiveTrivia}, + {VBasic.SyntaxKind.ElseDirectiveTrivia, CS.SyntaxKind.ElseDirectiveTrivia}, + {VBasic.SyntaxKind.EndIfDirectiveTrivia, CS.SyntaxKind.EndIfDirectiveTrivia}, + {VBasic.SyntaxKind.RegionDirectiveTrivia, CS.SyntaxKind.RegionDirectiveTrivia}, + {VBasic.SyntaxKind.EndRegionDirectiveTrivia, CS.SyntaxKind.EndRegionDirectiveTrivia}, + {VBasic.SyntaxKind.EnableWarningDirectiveTrivia, CS.SyntaxKind.WarningDirectiveTrivia}, + {VBasic.SyntaxKind.DisableWarningDirectiveTrivia, CS.SyntaxKind.WarningDirectiveTrivia}, + {VBasic.SyntaxKind.ReferenceDirectiveTrivia, CS.SyntaxKind.ReferenceDirectiveTrivia}, + {VBasic.SyntaxKind.BadDirectiveTrivia, CS.SyntaxKind.BadDirectiveTrivia}, + {VBasic.SyntaxKind.ConflictMarkerTrivia, CS.SyntaxKind.ConflictMarkerTrivia}, + {VBasic.SyntaxKind.ExternalSourceDirectiveTrivia, CS.SyntaxKind.LoadDirectiveTrivia}, //Just a guess + {VBasic.SyntaxKind.ExternalChecksumDirectiveTrivia, CS.SyntaxKind.LineDirectiveTrivia}, // Even more random guess }; - private static readonly Dictionary CSToVBSyntaxKinds = + private static readonly Dictionary CSToVBSyntaxKinds = VBToCSSyntaxKinds .ToLookup(kvp => kvp.Value, kvp => kvp.Key) .ToDictionary(g => g.Key, g => g.First()); - public static SyntaxKind? GetCSKind(this SyntaxTrivia t) + public static CS.SyntaxKind? GetCSKind(this SyntaxTrivia t) { - return VBToCSSyntaxKinds.TryGetValue(VBasic.VisualBasicExtensions.Kind(t), out var csKind) ? csKind : (SyntaxKind?)null; + return VBToCSSyntaxKinds.TryGetValue(VBasic.VisualBasicExtensions.Kind(t), out var csKind) ? csKind : (CS.SyntaxKind?)null; } public static VBasic.SyntaxKind? GetVBKind(this SyntaxTrivia t) { @@ -85,18 +86,18 @@ public static bool IsElastic(this SyntaxTrivia trivia) return trivia.HasAnnotation(SyntaxAnnotation.ElasticAnnotation); } - public static bool MatchesKind(this SyntaxTrivia trivia, SyntaxKind kind) + public static bool MatchesKind(this SyntaxTrivia trivia, CS.SyntaxKind kind) { return trivia.Kind() == kind; } - public static bool MatchesKind(this SyntaxTrivia trivia, SyntaxKind kind1, SyntaxKind kind2) + public static bool MatchesKind(this SyntaxTrivia trivia, CS.SyntaxKind kind1, CS.SyntaxKind kind2) { var triviaKind = trivia.Kind(); return triviaKind == kind1 || triviaKind == kind2; } - public static bool MatchesKind(this SyntaxTrivia trivia, params SyntaxKind[] kinds) + public static bool MatchesKind(this SyntaxTrivia trivia, params CS.SyntaxKind[] kinds) { return kinds.Contains(trivia.Kind()); } @@ -113,17 +114,17 @@ public static bool IsRegularOrDocComment(this SyntaxTrivia trivia) public static bool IsSingleLineComment(this SyntaxTrivia trivia) { - return trivia.Kind() == SyntaxKind.SingleLineCommentTrivia; + return trivia.Kind() == CS.SyntaxKind.SingleLineCommentTrivia; } public static bool IsMultiLineComment(this SyntaxTrivia trivia) { - return trivia.Kind() == SyntaxKind.MultiLineCommentTrivia; + return trivia.Kind() == CS.SyntaxKind.MultiLineCommentTrivia; } public static bool IsCompleteMultiLineComment(this SyntaxTrivia trivia) { - if (trivia.Kind() != SyntaxKind.MultiLineCommentTrivia) { + if (trivia.Kind() != CS.SyntaxKind.MultiLineCommentTrivia) { return false; } @@ -140,19 +141,28 @@ public static bool IsDocComment(this SyntaxTrivia trivia) public static bool IsSingleLineDocComment(this SyntaxTrivia trivia) { - return trivia.Kind() == SyntaxKind.SingleLineDocumentationCommentTrivia; + return trivia.Kind() == CS.SyntaxKind.SingleLineDocumentationCommentTrivia; } public static bool IsMultiLineDocComment(this SyntaxTrivia trivia) { - return trivia.Kind() == SyntaxKind.MultiLineDocumentationCommentTrivia; + return trivia.Kind() == CS.SyntaxKind.MultiLineDocumentationCommentTrivia; + } + + public static SyntaxTrivia GetEndOfLine(string lang) + { + if (lang == LanguageNames.CSharp) { + return CS.SyntaxFactory.ElasticCarriageReturnLineFeed; + } else { + return VBasic.SyntaxFactory.ElasticCarriageReturnLineFeed; + } } /// Good candidate for unit testing to catch newline issues hidden by the test harness public static string GetCommentText(this SyntaxTrivia trivia) { var commentText = trivia.ToString(); - if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) { + if (trivia.IsKind(CS.SyntaxKind.SingleLineCommentTrivia)) { if (commentText.StartsWith("//")) { commentText = commentText.Substring(2); } @@ -164,7 +174,7 @@ public static string GetCommentText(this SyntaxTrivia trivia) } return commentText.TrimStart(null); - } else if (trivia.Kind() == SyntaxKind.MultiLineCommentTrivia) { + } else if (trivia.Kind() == CS.SyntaxKind.MultiLineCommentTrivia) { var textBuilder = new StringBuilder(); if (commentText.EndsWith("*/")) { @@ -196,7 +206,7 @@ public static string GetCommentText(this SyntaxTrivia trivia) textBuilder.Remove(textBuilder.Length - newLine.Length, newLine.Length); return textBuilder.ToString(); - } else if (trivia.IsKind(VBasic.SyntaxKind.DocumentationCommentTrivia)) { + } else if (trivia.IsKind(VBasic.SyntaxKind.DocumentationCommentTrivia) || trivia.Kind() == CS.SyntaxKind.SingleLineDocumentationCommentTrivia) { var textBuilder = new StringBuilder(); if (commentText.EndsWith("*/")) { @@ -209,8 +219,7 @@ public static string GetCommentText(this SyntaxTrivia trivia) commentText = commentText.Trim(); - var newLine = commentText.Contains('\n') ? '\n' : '\r'; - var lines = commentText.Split(new []{newLine}, StringSplitOptions.None); + var lines = commentText.Replace("\r\n", "\n").Split('\n'); foreach (var line in lines) { var trimmedLine = line.Trim(); @@ -220,6 +229,10 @@ public static string GetCommentText(this SyntaxTrivia trivia) trimmedLine = trimmedLine.TrimStart('\''); trimmedLine = trimmedLine.TrimStart(null); } + if (trimmedLine.StartsWith("/")) { + trimmedLine = trimmedLine.TrimStart('/'); + trimmedLine = trimmedLine.TrimStart(null); + } textBuilder.AppendLine(trimmedLine); } @@ -251,7 +264,7 @@ public static int GetFullWidth(this IEnumerable trivia) public static SyntaxTriviaList AsTrivia(this string s) { - return SyntaxFactory.ParseLeadingTrivia(s ?? String.Empty); + return CS.SyntaxFactory.ParseLeadingTrivia(s ?? String.Empty); } public static bool IsWhitespaceOrEndOfLine(this SyntaxTrivia trivia) @@ -261,12 +274,12 @@ public static bool IsWhitespaceOrEndOfLine(this SyntaxTrivia trivia) public static bool IsEndOfLine(this SyntaxTrivia x) { - return x.IsKind(VBasic.SyntaxKind.EndOfLineTrivia) || x.IsKind(SyntaxKind.EndOfLineTrivia); + return x.IsKind(VBasic.SyntaxKind.EndOfLineTrivia) || x.IsKind(CS.SyntaxKind.EndOfLineTrivia); } - private static bool IsWhitespace(this SyntaxTrivia x) + public static bool IsWhitespace(this SyntaxTrivia x) { - return x.IsKind(VBasic.SyntaxKind.WhitespaceTrivia) || x.IsKind(SyntaxKind.WhitespaceTrivia); + return x.IsKind(VBasic.SyntaxKind.WhitespaceTrivia) || x.IsKind(CS.SyntaxKind.WhitespaceTrivia); } public static SyntaxTrivia GetPreviousTrivia( diff --git a/ICSharpCode.CodeConverter/VB/CSharpConverter.cs b/ICSharpCode.CodeConverter/VB/CSharpConverter.cs index 6e1beb6cc..fb7bede87 100644 --- a/ICSharpCode.CodeConverter/VB/CSharpConverter.cs +++ b/ICSharpCode.CodeConverter/VB/CSharpConverter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using ICSharpCode.CodeConverter.CSharp; @@ -9,10 +10,13 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Rename; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.VisualBasic; using CS = Microsoft.CodeAnalysis.CSharp; using CSS = Microsoft.CodeAnalysis.CSharp.Syntax; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; namespace ICSharpCode.CodeConverter.VB { @@ -25,12 +29,19 @@ public static async Task ConvertCompilationTree(Document document, var compilation = await document.Project.GetCompilationAsync(); var tree = await document.GetSyntaxTreeAsync(); var semanticModel = compilation.GetSemanticModel(tree, true); - var root = await document.GetSyntaxRootAsync() as CS.CSharpSyntaxNode ?? + var root = await document.GetSyntaxRootAsync() as CSS.CompilationUnitSyntax ?? throw new InvalidOperationException(NullRootError(document)); var vbSyntaxGenerator = SyntaxGenerator.GetGenerator(vbReferenceProject); - var visualBasicSyntaxVisitor = new NodesVisitor(document, (CS.CSharpCompilation)compilation, semanticModel, vbViewOfCsSymbols, vbSyntaxGenerator); - return root.Accept(visualBasicSyntaxVisitor.TriviaConvertingVisitor); + var numberOfLines = tree.GetLineSpan(root.FullSpan).EndLinePosition.Line; + + var visualBasicSyntaxVisitor = new NodesVisitor(document, (CS.CSharpCompilation)compilation, semanticModel, vbViewOfCsSymbols, vbSyntaxGenerator, numberOfLines); + var converted = root.Accept(visualBasicSyntaxVisitor.TriviaConvertingVisitor); + + var formattedConverted = (VBSyntax.CompilationUnitSyntax) Formatter.Format(converted, document.Project.Solution.Workspace); + + + return LineTriviaMapper.MapSourceTriviaToTarget(root, formattedConverted); } private static string NullRootError(Document document) @@ -40,6 +51,5 @@ private static string NullRootError(Document document) : "Could not find valid C# within document."; return initial + " For best results, convert a c# document from within a C# project which compiles successfully."; } - } } diff --git a/ICSharpCode.CodeConverter/VB/CommentConvertingMethodBodyVisitor.cs b/ICSharpCode.CodeConverter/VB/CommentConvertingMethodBodyVisitor.cs index 236d9276c..8c85bb9aa 100644 --- a/ICSharpCode.CodeConverter/VB/CommentConvertingMethodBodyVisitor.cs +++ b/ICSharpCode.CodeConverter/VB/CommentConvertingMethodBodyVisitor.cs @@ -8,38 +8,30 @@ using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; using SyntaxNodeExtensions = ICSharpCode.CodeConverter.Util.SyntaxNodeExtensions; using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; +using System.Linq; +using System.Collections; namespace ICSharpCode.CodeConverter.VB { public class CommentConvertingMethodBodyVisitor : CS.CSharpSyntaxVisitor> { private readonly CS.CSharpSyntaxVisitor> _wrappedVisitor; - private readonly TriviaConverter _triviaConverter; - public CommentConvertingMethodBodyVisitor(CS.CSharpSyntaxVisitor> wrappedVisitor, TriviaConverter triviaConverter) + public CommentConvertingMethodBodyVisitor(CS.CSharpSyntaxVisitor> wrappedVisitor) { this._wrappedVisitor = wrappedVisitor; - this._triviaConverter = triviaConverter; } public override SyntaxList DefaultVisit(SyntaxNode node) { try { - return ConvertWithTrivia(node); + var converted = _wrappedVisitor.Visit(node); + return converted.WithSourceMappingFrom(node); } catch (Exception e) { var dummyStatement = VBasic.SyntaxFactory.EmptyStatement(); var withVbTrailingErrorComment = dummyStatement.WithVbTrailingErrorComment((CS.CSharpSyntaxNode) node, e); return VBasic.SyntaxFactory.SingletonList(withVbTrailingErrorComment); } } - - private SyntaxList ConvertWithTrivia(SyntaxNode node) - { - var convertedNodes = _wrappedVisitor.Visit(node); - if (!convertedNodes.Any()) return convertedNodes; - // Port trivia to the last statement in the list - var lastWithConvertedTrivia = _triviaConverter.PortConvertedTrivia(node, convertedNodes.LastOrDefault()); - return convertedNodes.Replace(convertedNodes.LastOrDefault(), lastWithConvertedTrivia); - } } } diff --git a/ICSharpCode.CodeConverter/VB/CommentConvertingNodesVisitor.cs b/ICSharpCode.CodeConverter/VB/CommentConvertingNodesVisitor.cs index 0ee455392..725146741 100644 --- a/ICSharpCode.CodeConverter/VB/CommentConvertingNodesVisitor.cs +++ b/ICSharpCode.CodeConverter/VB/CommentConvertingNodesVisitor.cs @@ -10,35 +10,76 @@ using VbSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; using CsSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; using SyntaxFactory = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory; +using System.Collections; namespace ICSharpCode.CodeConverter.VB { - public class CommentConvertingNodesVisitor : CSharpSyntaxVisitor + internal class CommentConvertingNodesVisitor : CSharpSyntaxVisitor { - public TriviaConverter TriviaConverter { get; } private readonly CSharpSyntaxVisitor _wrappedVisitor; public CommentConvertingNodesVisitor(CSharpSyntaxVisitor wrappedVisitor) { - TriviaConverter = new TriviaConverter(); - this._wrappedVisitor = wrappedVisitor; + _wrappedVisitor = wrappedVisitor; } + public override VisualBasicSyntaxNode DefaultVisit(SyntaxNode node) { - try { - return TriviaConverter.PortConvertedTrivia(node, _wrappedVisitor.Visit(node)); - } catch (Exception e) { - var dummyStatement = SyntaxFactory.EmptyStatement(); - return dummyStatement.WithVbTrailingErrorComment((CSharpSyntaxNode) node, e); - } + return DefaultVisitInner(node); + } + private VisualBasicSyntaxNode DefaultVisitInner(SyntaxNode node) + { + return _wrappedVisitor.Visit(node); } public override VisualBasicSyntaxNode VisitAttributeList(CsSyntax.AttributeListSyntax node) { - var convertedNode = _wrappedVisitor.Visit(node) + var convertedNode = DefaultVisitInner(node) .WithPrependedLeadingTrivia(SyntaxFactory.EndOfLineTrivia(Environment.NewLine)); - return TriviaConverter.PortConvertedTrivia(node, convertedNode); + return convertedNode; + } + + public override VisualBasicSyntaxNode VisitCompilationUnit(CsSyntax.CompilationUnitSyntax node) + { + var convertedNode = (VbSyntax.CompilationUnitSyntax)DefaultVisitInner(node); + // Special case where we need to map manually because it's a special zero-width token that just has leading trivia that isn't at the start of the line necessarily + return convertedNode.WithEndOfFileToken( + convertedNode.EndOfFileToken.WithSourceMappingFrom(node.EndOfFileToken) + ); + } + + public override VisualBasicSyntaxNode VisitNamespaceDeclaration(CsSyntax.NamespaceDeclarationSyntax node) + { + var convertedNode = (VbSyntax.NamespaceBlockSyntax)DefaultVisitInner(node); + return convertedNode.WithEndNamespaceStatement( + convertedNode.EndNamespaceStatement.WithSourceMappingFrom(node.CloseBraceToken) + ); + } + + public override VisualBasicSyntaxNode VisitClassDeclaration(CsSyntax.ClassDeclarationSyntax node) + { + return WithMappedBlockEnd(node); + } + + public override VisualBasicSyntaxNode VisitStructDeclaration(CsSyntax.StructDeclarationSyntax node) + { + return WithMappedBlockEnd(node); + } + + public override VisualBasicSyntaxNode VisitEnumDeclaration(CsSyntax.EnumDeclarationSyntax node) + { + var convertedNode = (VbSyntax.EnumBlockSyntax)DefaultVisitInner(node); + return convertedNode.WithEndEnumStatement( + convertedNode.EndEnumStatement.WithSourceMappingFrom(node.CloseBraceToken) + ); + } + private VisualBasicSyntaxNode WithMappedBlockEnd(CsSyntax.BaseTypeDeclarationSyntax node) + { + var convertedNode = (VbSyntax.TypeBlockSyntax)DefaultVisitInner(node); + return convertedNode.WithEndBlockStatement( + convertedNode.EndBlockStatement.WithSourceMappingFrom(node.CloseBraceToken) + ); } } } diff --git a/ICSharpCode.CodeConverter/VB/CommentConvertingVisitorWrapper.cs b/ICSharpCode.CodeConverter/VB/CommentConvertingVisitorWrapper.cs new file mode 100644 index 000000000..1416b59a8 --- /dev/null +++ b/ICSharpCode.CodeConverter/VB/CommentConvertingVisitorWrapper.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using ICSharpCode.CodeConverter.CSharp; +using ICSharpCode.CodeConverter.Shared; +using ICSharpCode.CodeConverter.Util; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using VbSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; +using CsSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; +using SyntaxFactory = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory; +using System.Collections; + +namespace ICSharpCode.CodeConverter.VB +{ + + internal class CommentConvertingVisitorWrapper where T : VisualBasicSyntaxNode + { + private readonly CSharpSyntaxVisitor _wrappedVisitor; + public CommentConvertingVisitorWrapper(CSharpSyntaxVisitor wrappedVisitor) + { + _wrappedVisitor = wrappedVisitor; + } + + public TriviaConverter TriviaConverter { get; } + + public T Accept(SyntaxNode node, bool addSourceMapping) + { + try { + var converted = _wrappedVisitor.Visit(node); + return addSourceMapping ? node.CopyAnnotationsTo(converted).WithSourceMappingFrom(node) + : WithoutSourceMapping(converted); + } catch (Exception e) { + var dummyStatement = SyntaxFactory.EmptyStatement(); + return ((T)(object)dummyStatement).WithVbTrailingErrorComment((CSharpSyntaxNode)node, e); + } + + } + + private T WithoutSourceMapping(T converted) + { + converted = converted.ReplaceTokens(converted.DescendantTokens(), (o, r) => + r.WithoutAnnotations(AnnotationConstants.SourceStartLineAnnotationKind).WithoutAnnotations(AnnotationConstants.SourceEndLineAnnotationKind) + ); + return converted.ReplaceNodes(converted.DescendantNodes(), (o, r) => + r.WithoutAnnotations(AnnotationConstants.SourceStartLineAnnotationKind).WithoutAnnotations(AnnotationConstants.SourceEndLineAnnotationKind) + ); + } + } +} diff --git a/ICSharpCode.CodeConverter/VB/CommonConversions.cs b/ICSharpCode.CodeConverter/VB/CommonConversions.cs index 2b3925f53..9f7b4f7dc 100644 --- a/ICSharpCode.CodeConverter/VB/CommonConversions.cs +++ b/ICSharpCode.CodeConverter/VB/CommonConversions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using ICSharpCode.CodeConverter.Shared; @@ -11,6 +12,7 @@ using AttributeListSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax.AttributeListSyntax; using BinaryExpressionSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.BinaryExpressionSyntax; using CSharpExtensions = Microsoft.CodeAnalysis.CSharp.CSharpExtensions; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; using CSSyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; using CS = Microsoft.CodeAnalysis.CSharp; using CSS = Microsoft.CodeAnalysis.CSharp.Syntax; @@ -34,18 +36,15 @@ namespace ICSharpCode.CodeConverter.VB internal class CommonConversions { public SyntaxGenerator VbSyntaxGenerator { get; } - private readonly CS.CSharpSyntaxVisitor _nodesVisitor; - private readonly TriviaConverter _triviaConverter; + private readonly CommentConvertingVisitorWrapper _nodesVisitor; private readonly SemanticModel _semanticModel; public CommonConversions(SemanticModel semanticModel, SyntaxGenerator vbSyntaxGenerator, - CS.CSharpSyntaxVisitor nodesVisitor, - TriviaConverter triviaConverter) + CommentConvertingVisitorWrapper nodesVisitor) { VbSyntaxGenerator = vbSyntaxGenerator; _semanticModel = semanticModel; _nodesVisitor = nodesVisitor; - _triviaConverter = triviaConverter; } public SyntaxList ConvertBody(CSS.BlockSyntax body, @@ -113,7 +112,7 @@ private IEnumerable ConvertToDeclarationStatement(List variableDeclaratorSyntaxs = des.Select(ConvertToVariableDeclarator) .Concat(isPatternExpressions.Select(ConvertToVariableDeclaratorOrNull).Where(x => x != null)); var variableDeclaratorSyntaxes = variableDeclaratorSyntaxs.ToArray(); - return variableDeclaratorSyntaxes.Any() ? new StatementSyntax[]{CreateLocalDeclarationStatement(variableDeclaratorSyntaxes)} : Enumerable.Empty(); + return variableDeclaratorSyntaxes.Any() ? new StatementSyntax[] { CreateLocalDeclarationStatement(variableDeclaratorSyntaxes) } : Enumerable.Empty(); } private IEnumerable ConvertToDeclarationStatement(List propertyBlocks, bool isModule) @@ -165,20 +164,20 @@ private VariableDeclaratorSyntax ConvertToVariableDeclaratorOrNull(CSS.IsPattern { switch (node.Pattern) { case CSS.DeclarationPatternSyntax d: { - var id = ((IdentifierNameSyntax)d.Designation.Accept(_nodesVisitor)).Identifier; - var ids = SyntaxFactory.SingletonSeparatedList(SyntaxFactory.ModifiedIdentifier(id)); - TypeSyntax right = (TypeSyntax)d.Type.Accept(_nodesVisitor); - - var simpleAsClauseSyntax = SyntaxFactory.SimpleAsClause(right); - var equalsValueSyntax = SyntaxFactory.EqualsValue( - SyntaxFactory.LiteralExpression(SyntaxKind.NothingLiteralExpression, - SyntaxFactory.Token(SyntaxKind.NothingKeyword))); - return SyntaxFactory.VariableDeclarator(ids, simpleAsClauseSyntax, equalsValueSyntax); - } + var id = ((IdentifierNameSyntax)d.Designation.Accept(_nodesVisitor)).Identifier; + var ids = SyntaxFactory.SingletonSeparatedList(SyntaxFactory.ModifiedIdentifier(id)); + TypeSyntax right = (TypeSyntax)d.Type.Accept(_nodesVisitor); + + var simpleAsClauseSyntax = SyntaxFactory.SimpleAsClause(right); + var equalsValueSyntax = SyntaxFactory.EqualsValue( + SyntaxFactory.LiteralExpression(SyntaxKind.NothingLiteralExpression, + SyntaxFactory.Token(SyntaxKind.NothingKeyword))); + return SyntaxFactory.VariableDeclarator(ids, simpleAsClauseSyntax, equalsValueSyntax); + } case CSS.ConstantPatternSyntax _: return null; default: - throw new ArgumentOutOfRangeException(nameof(node.Pattern), node.Pattern, null); + throw new ArgumentOutOfRangeException(nameof(node.Pattern), node.Pattern, null); } } @@ -201,7 +200,7 @@ private VariableDeclaratorSyntax ConvertToVariableDeclarator(CSS.PropertyDeclara private CS.CSharpSyntaxVisitor> CreateMethodBodyVisitor(MethodBodyExecutableStatementVisitor methodBodyExecutableStatementVisitor = null) { - var visitor = methodBodyExecutableStatementVisitor ?? new MethodBodyExecutableStatementVisitor(_semanticModel, _nodesVisitor, _triviaConverter, this); + var visitor = methodBodyExecutableStatementVisitor ?? new MethodBodyExecutableStatementVisitor(_semanticModel, _nodesVisitor, this); return visitor.CommentConvertingVisitor; } @@ -212,7 +211,7 @@ public AccessorBlockSyntax ConvertAccessor(CSS.AccessorDeclarationSyntax node, o EndBlockStatementSyntax endStmt; SyntaxList body; isIterator = false; - var isIteratorState = new MethodBodyExecutableStatementVisitor(_semanticModel, _nodesVisitor, _triviaConverter, this); + var isIteratorState = new MethodBodyExecutableStatementVisitor(_semanticModel, _nodesVisitor, this); body = ConvertBody(node.Body, node.ExpressionBody, isIteratorState); isIterator = isIteratorState.IsIterator; var attributes = SyntaxFactory.List(node.AttributeLists.Select(a => (AttributeListSyntax)a.Accept(_nodesVisitor))); @@ -233,19 +232,23 @@ public AccessorBlockSyntax ConvertAccessor(CSS.AccessorDeclarationSyntax node, o case CSSyntaxKind.SetAccessorDeclaration: blockKind = SyntaxKind.SetAccessorBlock; valueParam = SyntaxFactory.Parameter(SyntaxFactory.ModifiedIdentifier("value")) - .WithAsClause(SyntaxFactory.SimpleAsClause((TypeSyntax)parent.Type.Accept(_nodesVisitor))) + .WithAsClause(SyntaxFactory.SimpleAsClause((TypeSyntax)parent.Type.Accept(_nodesVisitor, false))) .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ByValKeyword))); stmt = SyntaxFactory.SetAccessorStatement(attributes, modifiers, SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList(valueParam))); endStmt = SyntaxFactory.EndSetStatement(); if (isAutoImplementedProperty) { body = body.Count > 0 ? body : - SyntaxFactory.SingletonList((StatementSyntax)SyntaxFactory.AssignmentStatement(SyntaxKind.SimpleAssignmentStatement, SyntaxFactory.IdentifierName(GetVbPropertyBackingFieldName(parent)), SyntaxFactory.Token(VBUtil.GetExpressionOperatorTokenKind(SyntaxKind.SimpleAssignmentStatement)), SyntaxFactory.IdentifierName("value"))); + SyntaxFactory.SingletonList((StatementSyntax)SyntaxFactory.AssignmentStatement(SyntaxKind.SimpleAssignmentStatement, + SyntaxFactory.IdentifierName(GetVbPropertyBackingFieldName(parent)), + SyntaxFactory.Token(VBUtil.GetExpressionOperatorTokenKind(SyntaxKind.SimpleAssignmentStatement)), + SyntaxFactory.IdentifierName("value") + )); } break; case CSSyntaxKind.AddAccessorDeclaration: blockKind = SyntaxKind.AddHandlerAccessorBlock; valueParam = SyntaxFactory.Parameter(SyntaxFactory.ModifiedIdentifier("value")) - .WithAsClause(SyntaxFactory.SimpleAsClause((TypeSyntax)parent.Type.Accept(_nodesVisitor))) + .WithAsClause(SyntaxFactory.SimpleAsClause((TypeSyntax)parent.Type.Accept(_nodesVisitor, false))) .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ByValKeyword))); stmt = SyntaxFactory.AddHandlerAccessorStatement(attributes, modifiers, SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList(valueParam))); endStmt = SyntaxFactory.EndAddHandlerStatement(); @@ -253,7 +256,7 @@ public AccessorBlockSyntax ConvertAccessor(CSS.AccessorDeclarationSyntax node, o case CSSyntaxKind.RemoveAccessorDeclaration: blockKind = SyntaxKind.RemoveHandlerAccessorBlock; valueParam = SyntaxFactory.Parameter(SyntaxFactory.ModifiedIdentifier("value")) - .WithAsClause(SyntaxFactory.SimpleAsClause((TypeSyntax)parent.Type.Accept(_nodesVisitor))) + .WithAsClause(SyntaxFactory.SimpleAsClause((TypeSyntax)parent.Type.Accept(_nodesVisitor, false))) .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ByValKeyword))); stmt = SyntaxFactory.RemoveHandlerAccessorStatement(attributes, modifiers, SyntaxFactory.ParameterList(SyntaxFactory.SingletonSeparatedList(valueParam))); endStmt = SyntaxFactory.EndRemoveHandlerStatement(); @@ -261,7 +264,7 @@ public AccessorBlockSyntax ConvertAccessor(CSS.AccessorDeclarationSyntax node, o default: throw new NotSupportedException(); } - return SyntaxFactory.AccessorBlock(blockKind, stmt, body, endStmt); + return SyntaxFactory.AccessorBlock(blockKind, stmt, body, endStmt).WithSourceMappingFrom(node); } private static SyntaxToken GetVbPropertyBackingFieldName(CSS.BasePropertyDeclarationSyntax parent) @@ -282,7 +285,7 @@ public ExpressionSyntax ReduceArrayUpperBoundExpression(Microsoft.CodeAnalysis.C public LambdaExpressionSyntax ConvertLambdaExpression(CSS.AnonymousFunctionExpressionSyntax node, CS.CSharpSyntaxNode body, IEnumerable parameters, SyntaxTokenList modifiers) { - var symbol = (IMethodSymbol) ModelExtensions.GetSymbolInfo(_semanticModel, node).Symbol; + var symbol = (IMethodSymbol)ModelExtensions.GetSymbolInfo(_semanticModel, node).Symbol; var parameterList = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(parameters.Select(p => (Microsoft.CodeAnalysis.VisualBasic.Syntax.ParameterSyntax)p.Accept(_nodesVisitor)))); LambdaHeaderSyntax header; EndBlockStatementSyntax endBlock; @@ -314,7 +317,7 @@ public LambdaExpressionSyntax ConvertLambdaExpression(CSS.AnonymousFunctionExpre SyntaxFactory.SingletonList(vbThrowStatement), endBlock); } else { var stmt = GetStatementSyntax(body.Accept(_nodesVisitor), - expression => isSub ? (StatementSyntax) SyntaxFactory.ExpressionStatement(expression) : SyntaxFactory.ReturnStatement(expression)); + expression => isSub ? (StatementSyntax)SyntaxFactory.ExpressionStatement(expression) : SyntaxFactory.ReturnStatement(expression)); statements = InsertRequiredDeclarations(SyntaxFactory.SingletonList(stmt), body); } @@ -331,14 +334,15 @@ private static LambdaExpressionSyntax CreateLambdaExpression(SyntaxKind singleLi SyntaxKind multiLineExpressionKind, LambdaHeaderSyntax header, SyntaxList statements, EndBlockStatementSyntax endBlock) { - if (statements.Count == 1 && TryGetNodeForeSingleLineLambdaExpression(singleLineKind, statements[0], out VisualBasicSyntaxNode singleNode)) { + if (statements.Count == 1 && TryGetNodeForeSingleLineLambdaExpression(singleLineKind, statements[0], out VisualBasicSyntaxNode singleNode)) { return SyntaxFactory.SingleLineLambdaExpression(singleLineKind, header, singleNode); } return SyntaxFactory.MultiLineLambdaExpression(multiLineExpressionKind, header, statements, endBlock); } - private static bool TryGetNodeForeSingleLineLambdaExpression(SyntaxKind kind, StatementSyntax statement, out VisualBasicSyntaxNode singleNode) { + private static bool TryGetNodeForeSingleLineLambdaExpression(SyntaxKind kind, StatementSyntax statement, out VisualBasicSyntaxNode singleNode) + { switch (kind) { case SyntaxKind.SingleLineSubLambdaExpression when statement.DescendantNodesAndSelf().OfType().Count() == 1: singleNode = statement; @@ -508,7 +512,7 @@ private ModifiedIdentifierSyntax ExtractIdentifier(Microsoft.CodeAnalysis.CSharp public SyntaxToken ConvertIdentifier(SyntaxToken id) { - var parent = (CS.CSharpSyntaxNode) id.Parent; + var parent = (CS.CSharpSyntaxNode)id.Parent; var idText = AdjustIfEventIdentifier(id, parent); // Underscore is a special character in VB lexer which continues lines - not sure where to find the whole set of other similar tokens if any // Rather than a complicated contextual rename, just add an extra dash to all identifiers and hope this method is consistently used @@ -521,7 +525,7 @@ public SyntaxToken ConvertIdentifier(SyntaxToken id) idText = "Item"; break; } - return Identifier(idText, keywordRequiresEscaping); + return Identifier(idText, keywordRequiresEscaping).WithSourceMappingFrom(id); } private string AdjustIfEventIdentifier(SyntaxToken id, CS.CSharpSyntaxNode parent) @@ -701,8 +705,7 @@ public string GetFullyQualifiedName(INamespaceOrTypeSymbol symbol) public NameSyntax GetFullyQualifiedNameSyntax(INamespaceOrTypeSymbol symbol, bool allowGlobalPrefix = true) { - switch (symbol) - { + switch (symbol) { case ITypeSymbol ts: var nameSyntax = (NameSyntax)VbSyntaxGenerator.TypeExpression(ts); if (allowGlobalPrefix) diff --git a/ICSharpCode.CodeConverter/VB/CsExpander.cs b/ICSharpCode.CodeConverter/VB/CsExpander.cs index f310e107d..c1a28b02c 100644 --- a/ICSharpCode.CodeConverter/VB/CsExpander.cs +++ b/ICSharpCode.CodeConverter/VB/CsExpander.cs @@ -29,7 +29,7 @@ private SyntaxNode WithoutGlobalOverqualification(SyntaxNode node, SyntaxNode ex var aliasNodes = expandedNode.GetAnnotatedNodes(Simplifier.Annotation).Select(syntaxNode => LeftMostDescendant(syntaxNode).Parent).OfType().Where(n => n.Alias.IsGlobalId()).ToArray(); if (aliasNodes.Any()) { - return expandedNode.ReplaceNodes(aliasNodes, (orig, rewrite) => rewrite.Name); + return expandedNode.ReplaceNodes(aliasNodes, (orig, rewrite) => rewrite.Name.WithLeadingTrivia(rewrite.GetLeadingTrivia())); } return expandedNode; } diff --git a/ICSharpCode.CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs b/ICSharpCode.CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs index adc285675..4428a3ea0 100644 --- a/ICSharpCode.CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs +++ b/ICSharpCode.CodeConverter/VB/MethodBodyExecutableStatementVisitor.cs @@ -29,7 +29,7 @@ namespace ICSharpCode.CodeConverter.VB internal class MethodBodyExecutableStatementVisitor : CS.CSharpSyntaxVisitor> { private SemanticModel _semanticModel; - private readonly CS.CSharpSyntaxVisitor _nodesVisitor; + private readonly CommentConvertingVisitorWrapper _nodesVisitor; private readonly CommonConversions _commonConversions; private readonly Stack _blockInfo = new Stack(); // currently only works with switch blocks private int _switchCount = 0; @@ -42,13 +42,12 @@ private class BlockInfo public CommentConvertingMethodBodyVisitor CommentConvertingVisitor { get; } public MethodBodyExecutableStatementVisitor(SemanticModel semanticModel, - CSharpSyntaxVisitor nodesVisitor, TriviaConverter triviaConverter, - CommonConversions commonConversions) + CommentConvertingVisitorWrapper nodesVisitor, CommonConversions commonConversions) { this._semanticModel = semanticModel; this._nodesVisitor = nodesVisitor; _commonConversions = commonConversions; - CommentConvertingVisitor = new CommentConvertingMethodBodyVisitor(this, triviaConverter); + CommentConvertingVisitor = new CommentConvertingMethodBodyVisitor(this); } public override SyntaxList DefaultVisit(SyntaxNode node) diff --git a/ICSharpCode.CodeConverter/VB/NodesVisitor.cs b/ICSharpCode.CodeConverter/VB/NodesVisitor.cs index 77a7d25a3..f98707527 100644 --- a/ICSharpCode.CodeConverter/VB/NodesVisitor.cs +++ b/ICSharpCode.CodeConverter/VB/NodesVisitor.cs @@ -39,6 +39,7 @@ using static ICSharpCode.CodeConverter.VB.SyntaxKindExtensions; using SyntaxNodeExtensions = ICSharpCode.CodeConverter.Util.SyntaxNodeExtensions; using Microsoft.VisualBasic; +using System.Collections; namespace ICSharpCode.CodeConverter.VB { @@ -52,40 +53,29 @@ internal class NodesVisitor : CS.CSharpSyntaxVisitor private readonly SemanticModel _semanticModel; private readonly VisualBasicCompilation _vbViewOfCsSymbols; private readonly SyntaxGenerator _vbSyntaxGenerator; - - private readonly List _convertedImports = new List(); + private readonly List _importsToConvert = new List(); private readonly HashSet _extraImports = new HashSet(); - - - private int _placeholder = 1; private readonly CSharpHelperMethodDefinition _cSharpHelperMethodDefinition; private readonly CommonConversions _commonConversions; - public CommentConvertingNodesVisitor TriviaConvertingVisitor { get; } + + private int _placeholder = 1; + public CommentConvertingVisitorWrapper TriviaConvertingVisitor { get; } private string GeneratePlaceholder(string v) { return $"__{v}{_placeholder++}__"; } - private IEnumerable TidyImportsList(IEnumerable allImports) - { - return allImports - .SelectMany(x => x.ImportsClauses) - .GroupBy(x => x.ToString()) - .Select(g => g.First()) - .Select(x => SyntaxFactory.ImportsStatement(SyntaxFactory.SingletonSeparatedList(x))); - } - public NodesVisitor(Document document, CS.CSharpCompilation compilation, SemanticModel semanticModel, - VisualBasicCompilation vbViewOfCsSymbols, SyntaxGenerator vbSyntaxGenerator) + VisualBasicCompilation vbViewOfCsSymbols, SyntaxGenerator vbSyntaxGenerator, int numberOfLines) { _document = document; _compilation = compilation; _semanticModel = semanticModel; _vbViewOfCsSymbols = vbViewOfCsSymbols; _vbSyntaxGenerator = vbSyntaxGenerator; - TriviaConvertingVisitor = new CommentConvertingNodesVisitor(this); - _commonConversions = new CommonConversions(semanticModel, vbSyntaxGenerator, TriviaConvertingVisitor, TriviaConvertingVisitor.TriviaConverter); + TriviaConvertingVisitor = new CommentConvertingVisitorWrapper(new CommentConvertingNodesVisitor(this)); + _commonConversions = new CommonConversions(semanticModel, vbSyntaxGenerator, TriviaConvertingVisitor); _cSharpHelperMethodDefinition = new CSharpHelperMethodDefinition(); } @@ -95,22 +85,16 @@ public override VisualBasicSyntaxNode DefaultVisit(SyntaxNode node) .WithNodeInformation(node); } - private Func DelegateConversion(Func> convert) - { - return node => MethodBodyExecutableStatementVisitor.CreateBlock(convert(node)); - } - public override VisualBasicSyntaxNode VisitCompilationUnit(CSS.CompilationUnitSyntax node) { - foreach (var @using in node.Usings) - @using.Accept(TriviaConvertingVisitor); + _importsToConvert.AddRange(node.Usings); foreach (var @extern in node.Externs) @extern.Accept(TriviaConvertingVisitor); var attributes = SyntaxFactory.List(node.AttributeLists.Select(a => SyntaxFactory.AttributesStatement(SyntaxFactory.SingletonList((AttributeListSyntax)a.Accept(TriviaConvertingVisitor))))); var members = SyntaxFactory.List(node.Members.Select(m => (StatementSyntax)m.Accept(TriviaConvertingVisitor))); //TODO Add Usings from compilationoptions - var importsStatementSyntaxes = SyntaxFactory.List(TidyImportsList(_convertedImports.Concat(_extraImports.Select(Import)))); + var importsStatementSyntaxes = SyntaxFactory.List(_importsToConvert.Select(import => (ImportsStatementSyntax) import.Accept(TriviaConvertingVisitor)).Concat(_extraImports.Select(Import))); return SyntaxFactory.CompilationUnit( SyntaxFactory.List(), importsStatementSyntaxes, @@ -173,7 +157,7 @@ public override VisualBasicSyntaxNode VisitAttributeArgument(CSS.AttributeArgume public override VisualBasicSyntaxNode VisitNamespaceDeclaration(CSS.NamespaceDeclarationSyntax node) { foreach (var @using in node.Usings) - @using.Accept(TriviaConvertingVisitor); + _importsToConvert.AddRange(node.Usings); foreach (var @extern in node.Externs) @extern.Accept(TriviaConvertingVisitor); var members = node.Members.Select(m => (StatementSyntax)m.Accept(TriviaConvertingVisitor)); @@ -198,8 +182,7 @@ public override VisualBasicSyntaxNode VisitUsingDirective(CSS.UsingDirectiveSynt clause = clause.WithAlias(alias); } - _convertedImports.Add(SyntaxFactory.ImportsStatement(SyntaxFactory.SingletonSeparatedList(clause))); - return null; + return SyntaxFactory.ImportsStatement(SyntaxFactory.SingletonSeparatedList(clause)); } #region Namespace Members @@ -430,7 +413,7 @@ public override VisualBasicSyntaxNode VisitDestructorDeclaration(CSS.DestructorD public override VisualBasicSyntaxNode VisitMethodDeclaration(CSS.MethodDeclarationSyntax node) { - var isIteratorState = new MethodBodyExecutableStatementVisitor(_semanticModel, TriviaConvertingVisitor, TriviaConvertingVisitor.TriviaConverter, _commonConversions); + var isIteratorState = new MethodBodyExecutableStatementVisitor(_semanticModel, TriviaConvertingVisitor, _commonConversions); bool requiresBody = node.Body != null || node.ExpressionBody != null || node.Modifiers.Any(m => SyntaxTokenExtensions.IsKind(m, CS.SyntaxKind.ExternKeyword)); var block = _commonConversions.ConvertBody(node.Body, node.ExpressionBody, isIteratorState); var id = _commonConversions.ConvertIdentifier(node.Identifier); @@ -631,7 +614,7 @@ public override VisualBasicSyntaxNode VisitEventDeclaration(CSS.EventDeclaration .OfType() .SelectMany(x => x.Left.DescendantNodesAndSelf().OfType()) .FirstOrDefault(); - var eventFieldIdentifier = (IdentifierNameSyntax)csEventFieldIdentifier?.Accept(TriviaConvertingVisitor); + var eventFieldIdentifier = (IdentifierNameSyntax)csEventFieldIdentifier?.Accept(TriviaConvertingVisitor, false); var riseEventAccessor = SyntaxFactory.RaiseEventAccessorBlock( SyntaxFactory.RaiseEventAccessorStatement( diff --git a/ICSharpCode.CodeConverter/VB/SyntaxNodeVisitorExtensions.cs b/ICSharpCode.CodeConverter/VB/SyntaxNodeVisitorExtensions.cs new file mode 100644 index 000000000..4cae31ce4 --- /dev/null +++ b/ICSharpCode.CodeConverter/VB/SyntaxNodeVisitorExtensions.cs @@ -0,0 +1,25 @@ +using System; +using ICSharpCode.CodeConverter.CSharp; +using ICSharpCode.CodeConverter.Shared; +using ICSharpCode.CodeConverter.Util; +using Microsoft.CodeAnalysis; +using CS = Microsoft.CodeAnalysis.CSharp; +using VBasic = Microsoft.CodeAnalysis.VisualBasic; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; +using SyntaxNodeExtensions = ICSharpCode.CodeConverter.Util.SyntaxNodeExtensions; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; +using System.Linq; +using System.Collections; + +namespace ICSharpCode.CodeConverter.VB +{ + + internal static class SyntaxNodeVisitorExtensions + { + public static T Accept(this SyntaxNode node, CommentConvertingVisitorWrapper visitorWrapper, bool addSourceMapping = true) where T : VBasic.VisualBasicSyntaxNode + { + if (node == null) return default(T); + return visitorWrapper.Accept(node, addSourceMapping); + } + } +} diff --git a/Tests/CSharp/ExpressionTests.cs b/Tests/CSharp/ExpressionTests.cs index 02c3b785a..f6d677f96 100644 --- a/Tests/CSharp/ExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests.cs @@ -37,7 +37,6 @@ public void TestMethod() bool enumEnumEquality = eEnum == RankEnum.First; } }"); - } [Fact] @@ -58,7 +57,6 @@ public void TestMethod() private static int Val; }"); - } [Fact] @@ -2227,7 +2225,6 @@ public static void Main() "); } - [Fact] public async Task StringInterpolationWithConditionalOperator() { @@ -2620,6 +2617,5 @@ public static void OnError(object s, ErrorEventArgs e) }"); } - } } diff --git a/Tests/CSharp/LinqExpressionTests.cs b/Tests/CSharp/LinqExpressionTests.cs index 9e4940af4..d9af34765 100644 --- a/Tests/CSharp/LinqExpressionTests.cs +++ b/Tests/CSharp/LinqExpressionTests.cs @@ -14,7 +14,6 @@ public async Task Linq1() await TestConversionVisualBasicToCSharp(@"Private Shared Sub SimpleQuery() Dim numbers = {7, 9, 5, 3, 6} Dim res = From n In numbers Where n > 5 Select n - For Each n In res Console.WriteLine(n) Next @@ -25,7 +24,6 @@ For Each n In res var res = from n in numbers where n > 5 select n; - foreach (var n in res) Console.WriteLine(n); }"); diff --git a/Tests/CSharp/MissingSemanticModelInfo/ExpressionTests.cs b/Tests/CSharp/MissingSemanticModelInfo/ExpressionTests.cs index 6c2ae724c..e85a9c0ed 100644 --- a/Tests/CSharp/MissingSemanticModelInfo/ExpressionTests.cs +++ b/Tests/CSharp/MissingSemanticModelInfo/ExpressionTests.cs @@ -251,24 +251,25 @@ private void TestMethod() } [Fact] - public async Task CharacterizeRaiseEventWithMissingDefinitionActsLikeFunc() + public async Task CharacterizeRaiseEventWithMissingDefinitionActsLikeMultiIndexer() { - await TestConversionCSharpToVisualBasic( - @"using System; - -class TestClass -{ - void TestMethod() - { - if (MyEvent != null) MyEvent(this, EventArgs.Empty); - } -}", @"Imports System + await TestConversionVisualBasicToCSharp( + @"Imports System Friend Class TestClass Private Sub TestMethod() If MyEvent IsNot Nothing Then MyEvent(Me, EventArgs.Empty) End Sub -End Class"); +End Class", @"using System; + +internal partial class TestClass +{ + private void TestMethod() + { + if (MyEvent != null) + MyEvent[this, EventArgs.Empty]; + } +}"); } } } diff --git a/Tests/CSharp/NamespaceLevelTests.cs b/Tests/CSharp/NamespaceLevelTests.cs index 928345e04..0504021cf 100644 --- a/Tests/CSharp/NamespaceLevelTests.cs +++ b/Tests/CSharp/NamespaceLevelTests.cs @@ -7,7 +7,6 @@ namespace CodeConverter.Tests.CSharp { public class NamespaceLevelTests : ConverterTestBase { - [Fact] public async Task TestNamespace() { @@ -812,7 +811,6 @@ public static void Main() object vObjectULong = EULong.M1; } }"); - } [Fact] @@ -907,6 +905,5 @@ public void TestMethod() } }"); } - } } diff --git a/Tests/CSharp/StatementTests.cs b/Tests/CSharp/StatementTests.cs index f28ed119c..69a7c3e5e 100644 --- a/Tests/CSharp/StatementTests.cs +++ b/Tests/CSharp/StatementTests.cs @@ -1111,7 +1111,6 @@ public static void Main() }}"); } - [Fact] public async Task DeclareStatementWithAttributes() { diff --git a/Tests/LanguageAgnostic/ProjectFileTextEditorTests.cs b/Tests/LanguageAgnostic/ProjectFileTextEditorTests.cs index 0e8d76d7d..302100faf 100644 --- a/Tests/LanguageAgnostic/ProjectFileTextEditorTests.cs +++ b/Tests/LanguageAgnostic/ProjectFileTextEditorTests.cs @@ -11,7 +11,6 @@ namespace CodeConverter.Tests.LanguageAgnostic { public class ProjectFileTextEditorTests { - [Fact] public void TogglesExistingValue() { diff --git a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/Properties/AssemblyInfo.vb b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/Properties/AssemblyInfo.vb index 4987d6de6..05c12848a 100644 --- a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/Properties/AssemblyInfo.vb +++ b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/Properties/AssemblyInfo.vb @@ -1,6 +1,10 @@ Imports System.Reflection Imports System.Runtime.InteropServices + +' General Information about an assembly is controlled through the following +' set of attributes. Change these attribute values to modify the information +' associated with an assembly. @@ -9,7 +13,24 @@ Imports System.Runtime.InteropServices + +' Setting ComVisible to false makes the types in this assembly not visible +' to COM components. If you need to access a type in this assembly from +' COM, set the ComVisible attribute to true on that type. + +' The following GUID is for the ID of the typelib if this project is exposed to COM + +' Version information for an assembly consists of the following four values: +' +' Major Version +' Minor Version +' Build Number +' Revision +' +' You can specify all the values or you can default the Build and Revision Numbers +' by using the '*' as shown below: +' [assembly: AssemblyVersion("1.0.*")] diff --git "a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" "b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" index 668a3737e..1a4513687 100644 --- "a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" +++ "b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertCSharpConsoleAppOnly/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" @@ -5,6 +5,7 @@ Namespace ConsoleApp2 Public Class ユニコードのプログラム Public Shared Sub Main() For Each process In Process.GetProcesses.Where(Function(p) String.IsNullOrEmpty(p.MainWindowTitle)).Take(1) + ' Here's a comment Console.WriteLine(-1) Next End Sub diff --git a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/Properties/AssemblyInfo.vb b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/Properties/AssemblyInfo.vb index 4987d6de6..05c12848a 100644 --- a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/Properties/AssemblyInfo.vb +++ b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/Properties/AssemblyInfo.vb @@ -1,6 +1,10 @@ Imports System.Reflection Imports System.Runtime.InteropServices + +' General Information about an assembly is controlled through the following +' set of attributes. Change these attribute values to modify the information +' associated with an assembly. @@ -9,7 +13,24 @@ Imports System.Runtime.InteropServices + +' Setting ComVisible to false makes the types in this assembly not visible +' to COM components. If you need to access a type in this assembly from +' COM, set the ComVisible attribute to true on that type. + +' The following GUID is for the ID of the typelib if this project is exposed to COM + +' Version information for an assembly consists of the following four values: +' +' Major Version +' Minor Version +' Build Number +' Revision +' +' You can specify all the values or you can default the Build and Revision Numbers +' by using the '*' as shown below: +' [assembly: AssemblyVersion("1.0.*")] diff --git "a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" "b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" index 668a3737e..1a4513687 100644 --- "a/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" +++ "b/Tests/TestData/MultiFileCharacterization/CSToVBResults/ConvertWholeSolution/ConsoleApp2/\343\203\246\343\203\213\343\202\263\343\203\274\343\203\211\343\201\256\343\203\227\343\203\255\343\202\260\343\203\251\343\203\240.vb" @@ -5,6 +5,7 @@ Namespace ConsoleApp2 Public Class ユニコードのプログラム Public Shared Sub Main() For Each process In Process.GetProcesses.Where(Function(p) String.IsNullOrEmpty(p.MainWindowTitle)).Take(1) + ' Here's a comment Console.WriteLine(-1) Next End Sub diff --git a/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertVbLibraryOnly/VbLibrary/My Project/Settings.Designer.cs b/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertVbLibraryOnly/VbLibrary/My Project/Settings.Designer.cs index 6b14969c9..5c447f663 100644 --- a/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertVbLibraryOnly/VbLibrary/My Project/Settings.Designer.cs +++ b/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertVbLibraryOnly/VbLibrary/My Project/Settings.Designer.cs @@ -20,7 +20,7 @@ internal sealed partial class MySettings : System.Configuration.ApplicationSetti { private static MySettings defaultInstance = (MySettings)Synchronized(new MySettings()); - /* TODO ERROR: Skipped IfDirectiveTrivia *//* TODO ERROR: Skipped DisabledTextTrivia *//* TODO ERROR: Skipped EndIfDirectiveTrivia */ + /* TODO ERROR: Skipped RegionDirectiveTrivia *//* TODO ERROR: Skipped IfDirectiveTrivia *//* TODO ERROR: Skipped DisabledTextTrivia *//* TODO ERROR: Skipped EndIfDirectiveTrivia *//* TODO ERROR: Skipped EndRegionDirectiveTrivia */ public static MySettings Default { get diff --git a/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertWholeSolution/VbLibrary/My Project/Settings.Designer.cs b/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertWholeSolution/VbLibrary/My Project/Settings.Designer.cs index 6b14969c9..5c447f663 100644 --- a/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertWholeSolution/VbLibrary/My Project/Settings.Designer.cs +++ b/Tests/TestData/MultiFileCharacterization/VBToCSResults/ConvertWholeSolution/VbLibrary/My Project/Settings.Designer.cs @@ -20,7 +20,7 @@ internal sealed partial class MySettings : System.Configuration.ApplicationSetti { private static MySettings defaultInstance = (MySettings)Synchronized(new MySettings()); - /* TODO ERROR: Skipped IfDirectiveTrivia *//* TODO ERROR: Skipped DisabledTextTrivia *//* TODO ERROR: Skipped EndIfDirectiveTrivia */ + /* TODO ERROR: Skipped RegionDirectiveTrivia *//* TODO ERROR: Skipped IfDirectiveTrivia *//* TODO ERROR: Skipped DisabledTextTrivia *//* TODO ERROR: Skipped EndIfDirectiveTrivia *//* TODO ERROR: Skipped EndRegionDirectiveTrivia */ public static MySettings Default { get diff --git a/Tests/TestRunners/ConverterTestBase.cs b/Tests/TestRunners/ConverterTestBase.cs index aa3b8c712..1869a98f3 100644 --- a/Tests/TestRunners/ConverterTestBase.cs +++ b/Tests/TestRunners/ConverterTestBase.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using ICSharpCode.CodeConverter; using ICSharpCode.CodeConverter.CSharp; @@ -16,9 +18,10 @@ namespace CodeConverter.Tests.TestRunners { public class ConverterTestBase { + private const string AutoTestCommentPrefix = " SourceLine:"; private static readonly bool RecharacterizeByWritingExpectedOverActual = TestConstants.RecharacterizeByWritingExpectedOverActual; - private bool _testCstoVBCommentsByDefault = false; + private bool _testCstoVBCommentsByDefault = true; private readonly string _rootNamespace; protected TextConversionOptions EmptyNamespaceOptionStrictOff { get; set; } @@ -42,12 +45,42 @@ public ConverterTestBase(string rootNamespace = null) return convertedCode; } - public async Task TestConversionCSharpToVisualBasic(string csharpCode, string expectedVisualBasicCode, bool expectSurroundingMethodBlock = false, bool expectCompilationErrors = false, TextConversionOptions conversion = null) + public async Task TestConversionCSharpToVisualBasic(string csharpCode, string expectedVisualBasicCode, bool expectSurroundingMethodBlock = false, bool expectCompilationErrors = false, TextConversionOptions conversion = null, bool hasLineCommentConversionIssue = false) { expectedVisualBasicCode = AddSurroundingMethodBlock(expectedVisualBasicCode, expectSurroundingMethodBlock); await TestConversionCSharpToVisualBasicWithoutComments(csharpCode, expectedVisualBasicCode, conversion); - if (_testCstoVBCommentsByDefault) await TestConversionCSharpToVisualBasicWithoutComments(AddLineNumberComments(csharpCode, "// ", false), AddLineNumberComments(expectedVisualBasicCode, "' ", true), conversion); + if (_testCstoVBCommentsByDefault && !hasLineCommentConversionIssue) { + await AssertLineCommentsConvertedInSameOrder(csharpCode, conversion, "//", LineCanHaveCSharpComment); + } + } + + private static bool LineCanHaveCSharpComment(string l) + { + return !l.TrimStart().StartsWith("#region"); + } + + /// + /// Lines that already have cooments aren't automatically tested, so if a line changes order in a conversion, just add a comment to that line. + /// If there's a comment conversion issue, set the optional hasLineCommentConversionIssue to true + /// + private async Task AssertLineCommentsConvertedInSameOrder(string source, TextConversionOptions conversion, string singleLineCommentStart, Func lineCanHaveComment) + { + var (sourceLinesWithComments, lineNumbersAdded) = AddLineNumberComments(source, singleLineCommentStart, AutoTestCommentPrefix, lineCanHaveComment); + string sourceWithComments = string.Join(Environment.NewLine, sourceLinesWithComments); + var convertedCode = await Convert(sourceWithComments, conversion); + var convertedCommentLineNumbers = convertedCode.Split(new[] { AutoTestCommentPrefix }, StringSplitOptions.None) + .Skip(1).Select(afterPrefix => afterPrefix.Split('\n')[0].TrimEnd()).ToList(); + var missingSourceLineNumbers = lineNumbersAdded.Except(convertedCommentLineNumbers); + if (missingSourceLineNumbers.Any()) { + Assert.False(true, "Comments not converted from source lines: " + string.Join(", ", missingSourceLineNumbers) + GetSourceAndConverted(sourceWithComments, convertedCode)); + } + OurAssert.Equal(string.Join(", ", lineNumbersAdded), string.Join(", ", convertedCommentLineNumbers), () => GetSourceAndConverted(sourceWithComments, convertedCode)); + } + + private static string GetSourceAndConverted(string sourceLinesWithComments, string convertedCode) + { + return "\r\n-------------------\r\nConverted:\r\n" + convertedCode + "\r\n\r\nSource:\r\n" + sourceLinesWithComments; } private static string AddSurroundingMethodBlock(string expectedVisualBasicCode, bool expectSurroundingBlock) @@ -71,11 +104,11 @@ private async Task TestConversionCSharpToVisualBasicWithoutComments(string cshar /// /// is currently unused but acts as documentation, and in future will be used to decide whether to check if the input/output compiles /// - public async Task TestConversionVisualBasicToCSharp(string visualBasicCode, string expectedCsharpCode, bool expectSurroundingBlock = false, bool missingSemanticInfo = false) + public async Task TestConversionVisualBasicToCSharp(string visualBasicCode, string expectedCsharpCode, bool expectSurroundingBlock = false, bool missingSemanticInfo = false, bool hasLineCommentConversionIssue = false) { if (expectSurroundingBlock) expectedCsharpCode = SurroundWithBlock(expectedCsharpCode); await TestConversionVisualBasicToCSharpWithoutComments(visualBasicCode, expectedCsharpCode); - await TestConversionVisualBasicToCSharpWithoutComments(AddLineNumberComments(visualBasicCode, "' ", false), AddLineNumberComments(expectedCsharpCode, "// ", true)); + if (!hasLineCommentConversionIssue) await TestConversionVisualBasicToCSharpWithoutComments(AddLineNumberComments(visualBasicCode, "' ", false), AddLineNumberComments(expectedCsharpCode, "// ", true)); } private static string SurroundWithBlock(string expectedCsharpCode) @@ -91,24 +124,28 @@ public async Task TestConversionVisualBasicToCSharpWithoutComments(string visual private async Task AssertConvertedCodeResultEquals(string inputCode, string expectedConvertedCode, TextConversionOptions conversionOptions = default) where TLanguageConversion : ILanguageConversion, new() { - var textConversionOptions = conversionOptions ?? new TextConversionOptions(DefaultReferences.NetStandard2){ RootNamespaceOverride = _rootNamespace }; - var outputNode = ProjectConversion.ConvertText(inputCode, textConversionOptions); - AssertConvertedCodeResultEquals(await outputNode, expectedConvertedCode, inputCode); + string convertedTextFollowedByExceptions = await Convert(inputCode, conversionOptions); + AssertConvertedCodeResultEquals(convertedTextFollowedByExceptions, expectedConvertedCode, inputCode); + } + + private async Task Convert(string inputCode, TextConversionOptions conversionOptions) where TLanguageConversion : ILanguageConversion, new() + { + var textConversionOptions = conversionOptions ?? new TextConversionOptions(DefaultReferences.NetStandard2) { RootNamespaceOverride = _rootNamespace }; + var conversionResult = await ProjectConversion.ConvertText(inputCode, textConversionOptions); + return (conversionResult.ConvertedCode ?? "") + (conversionResult.GetExceptionsAsString() ?? ""); } - private static void AssertConvertedCodeResultEquals(ConversionResult conversionResult, + private static void AssertConvertedCodeResultEquals(string convertedCodeFollowedByExceptions, string expectedConversionResultText, string originalSource) { - var convertedTextFollowedByExceptions = - (conversionResult.ConvertedCode ?? "") + (conversionResult.GetExceptionsAsString() ?? ""); - var txt = convertedTextFollowedByExceptions.TrimEnd(); + var txt = convertedCodeFollowedByExceptions.TrimEnd(); expectedConversionResultText = expectedConversionResultText.TrimEnd(); AssertCodeEqual(originalSource, expectedConversionResultText, txt); } private static void AssertCodeEqual(string originalSource, string expectedConversion, string actualConversion) { - OurAssert.StringsEqualIgnoringNewlines(expectedConversion, actualConversion, () => + OurAssert.EqualIgnoringNewlines(expectedConversion, actualConversion, () => { StringBuilder sb = OurAssert.DescribeStringDiff(expectedConversion, actualConversion); sb.AppendLine(); @@ -120,6 +157,22 @@ private static void AssertCodeEqual(string originalSource, string expectedConver Assert.False(RecharacterizeByWritingExpectedOverActual, $"Test setup issue: Set {nameof(RecharacterizeByWritingExpectedOverActual)} to false after using it"); } + + /// Currently puts comments in multi-line comments which then don't get converted + private static (IReadOnlyCollection Lines, IReadOnlyCollection LineNumbersAdded) AddLineNumberComments(string code, string singleLineCommentStart, string commentPrefix, Func lineCanHaveComment) + { + var lines = Utils.HomogenizeEol(code).Split(new[] { Environment.NewLine }, StringSplitOptions.None); + var lineNumbersAdded = new List(); + var newLines = lines.Select((line, i) => { + var lineNumber = i.ToString(); + var potentialExistingComments = line.Split(new[] { singleLineCommentStart }, StringSplitOptions.None).Skip(1); + if (potentialExistingComments.Count() == 1 || !lineCanHaveComment(line)) return line; + lineNumbersAdded.Add(lineNumber); + return line + singleLineCommentStart + commentPrefix + lineNumber; + }).ToArray(); + return (newLines, lineNumbersAdded); + } + private static string AddLineNumberComments(string code, string singleLineCommentStart, bool isTarget) { int skipped = 0; @@ -127,7 +180,6 @@ private static string AddLineNumberComments(string code, string singleLineCommen bool started = false; var newLines = lines.Select((s, i) => { - var prevLine = i > 0 ? lines[i - 1] : ""; var nextLine = i < lines.Length - 1 ? lines[i + 1] : ""; diff --git a/Tests/TestRunners/MultiFileTestFixture.cs b/Tests/TestRunners/MultiFileTestFixture.cs index cfb95f86c..8c792bc0d 100644 --- a/Tests/TestRunners/MultiFileTestFixture.cs +++ b/Tests/TestRunners/MultiFileTestFixture.cs @@ -201,7 +201,7 @@ private void AssertFileEqual(Dictionary conversionResu var conversionResult = conversionResults[convertedFilePath]; var actualText = conversionResult.ConvertedCode ?? "" + conversionResult.GetExceptionsAsString() ?? ""; - OurAssert.StringsEqualIgnoringNewlines(expectedText, actualText); + OurAssert.EqualIgnoringNewlines(expectedText, actualText); Assert.Equal(GetEncoding(expectedFile.FullName), GetEncoding(conversionResult)); } diff --git a/Tests/TestRunners/OurAssert.cs b/Tests/TestRunners/OurAssert.cs index 67b1c3a8e..ba7f1c229 100644 --- a/Tests/TestRunners/OurAssert.cs +++ b/Tests/TestRunners/OurAssert.cs @@ -4,17 +4,13 @@ namespace CodeConverter.Tests.TestRunners { - public class OurAssert + public static class OurAssert { public static StringBuilder DescribeStringDiff(string expectedConversion, string actualConversion) { int l = Math.Max(expectedConversion.Length, actualConversion.Length); - StringBuilder sb = new StringBuilder(l * 4); - sb.AppendLine("expected:"); - sb.AppendLine(expectedConversion); - sb.AppendLine("got:"); - sb.AppendLine(actualConversion); - sb.AppendLine("diff:"); + StringBuilder sb = new StringBuilder(l); + sb.AppendLine("------------------------------------\r\ndiff:"); for (int i = 0; i < l; i++) { if (i >= expectedConversion.Length || i >= actualConversion.Length || @@ -24,23 +20,34 @@ public static StringBuilder DescribeStringDiff(string expectedConversion, string sb.Append(expectedConversion[i]); } - return sb; + return sb.AppendLine("------------------------------------"); } - public static void StringsEqualIgnoringNewlines(string expectedText, string actualText) + public static void EqualIgnoringNewlines(string expectedText, string actualText) { - expectedText = Utils.HomogenizeEol(expectedText); - actualText = Utils.HomogenizeEol(actualText); - if (expectedText.Equals(actualText)) return; - Assert.True(false, DescribeStringDiff(expectedText, actualText).ToString()); + const string splitter = "\r\n------------------------------------\r\n"; + EqualIgnoringNewlines(expectedText + splitter, actualText + splitter, () => DescribeStringDiff(expectedText, actualText).ToString()); } - public static void StringsEqualIgnoringNewlines(string expectedText, string actualText, Func getMessage) + public static void EqualIgnoringNewlines(string expectedText, string actualText, Func getMessage) { expectedText = Utils.HomogenizeEol(expectedText); actualText = Utils.HomogenizeEol(actualText); - if (expectedText.Equals(actualText)) return; - Assert.True(false, getMessage()); + Equal(expectedText, actualText, getMessage); + } + + public static void Equal(object expectedText, object actualText, Func getMessage) + { + WithMessage(() => Assert.Equal(expectedText, actualText), getMessage); + } + + public static void WithMessage(Action assertion, Func getMessage) + { + try { + assertion(); + } catch (Exception e) { + throw new Exception(e.Message + "\r\n" + getMessage(), e); + } } } } diff --git a/Tests/TestRunners/SelfVerifyingTestFactory.cs b/Tests/TestRunners/SelfVerifyingTestFactory.cs index 3a182e793..40ee4b512 100644 --- a/Tests/TestRunners/SelfVerifyingTestFactory.cs +++ b/Tests/TestRunners/SelfVerifyingTestFactory.cs @@ -26,7 +26,7 @@ internal class SelfVerifyingTestFactory public static IEnumerable GetSelfVerifyingFacts(string testFilepath) where TSourceCompiler : ICompiler, new() where TTargetCompiler : ICompiler, new() where TLanguageConversion : ILanguageConversion, new() { - var sourceFileText = File.ReadAllText(testFilepath); + var sourceFileText = File.ReadAllText(testFilepath, Encoding.UTF8); var sourceCompiler = new TSourceCompiler(); var syntaxTree = sourceCompiler.CreateTree(sourceFileText).WithFilePath(Path.GetFullPath(testFilepath)); var compiledSource = sourceCompiler.AssemblyFromCode(syntaxTree, AdditionalReferences); diff --git a/Tests/UnicodeNewline.cs b/Tests/UnicodeNewline.cs index 35ded10f4..292009367 100644 --- a/Tests/UnicodeNewline.cs +++ b/Tests/UnicodeNewline.cs @@ -148,7 +148,6 @@ public static bool TryGetDelimiterLengthAndType(char curChar, out int length, ou } else { length = 1; type = UnicodeNewline.CR; - } return true; } @@ -201,7 +200,6 @@ public static bool TryGetDelimiterLengthAndType(char curChar, out int length, ou } else { length = 1; type = UnicodeNewline.CR; - } return true; } diff --git a/Tests/VB/ExpressionTests.cs b/Tests/VB/ExpressionTests.cs index ad4a50a30..925e6afdc 100644 --- a/Tests/VB/ExpressionTests.cs +++ b/Tests/VB/ExpressionTests.cs @@ -24,12 +24,11 @@ End Sub End Class"); } - [Fact] public async Task StringInterpolationWithDoubleQuotes() { await TestConversionCSharpToVisualBasic( - @"using System; + @"using System; //Not required in VB due to global imports namespace global::InnerNamespace { @@ -94,7 +93,7 @@ public bool Foo { }", @"Public Class Test Public ReadOnly Property Foo As Boolean Get - Return Bar Is Nothing + Return Bar Is Nothing ' Crashes conversion to VB End Get End Property @@ -233,7 +232,7 @@ string CreateProperty() { return """"; } }", -@"Friend Class TestClass + @"Friend Class TestClass Private prop As String Private prop2 As String @@ -324,7 +323,7 @@ void TestMethod() { x /= 3; } }", -@"Public Class TestClass + @"Public Class TestClass Private Sub TestMethod() Dim x As Integer = 10 x *= 3 @@ -658,7 +657,7 @@ where n > 5 foreach (var n in res) Console.WriteLine(n); }", -@"Private Shared Sub SimpleQuery() + @"Private Shared Sub SimpleQuery() Dim numbers As Integer() = {7, 9, 5, 3, 6} Dim res = From n In numbers Where n > 5 Select n @@ -692,7 +691,7 @@ group n by n % 5 into g } } }", -@"Public Shared Sub Linq40() + @"Public Shared Sub Linq40() Dim numbers As Integer() = {5, 4, 1, 3, 9, 8, 6, 7, 2, 0} Dim numberGroups = From n In numbers Group n By __groupByKey1__ = n Mod 5 Into g = Group Select New With { .Remainder = __groupByKey1__, @@ -742,7 +741,7 @@ join p in products on c equals p.Category } } }", -@"Friend Class Product + @"Friend Class Product Public Category As String Public ProductName As String End Class @@ -811,7 +810,6 @@ For Each p In v.Products } [Fact] public async Task MultilineSubExpressionWithSingleStatement() { - await TestConversionCSharpToVisualBasic( @"public class TestClass : System.Collections.ObjectModel.ObservableCollection { public TestClass() { @@ -823,7 +821,7 @@ public TestClass() { }; } }", -@"Public Class TestClass + @"Public Class TestClass Inherits ObjectModel.ObservableCollection(Of String) Public Sub New() @@ -852,7 +850,7 @@ public TestClass() { string str = create(this); } }", -@"Imports System + @"Imports System Public Class TestClass Private create As Func(Of Object, String) = Function(o) diff --git a/Tests/VB/MemberTests.cs b/Tests/VB/MemberTests.cs index 620e22b29..7a07b4606 100644 --- a/Tests/VB/MemberTests.cs +++ b/Tests/VB/MemberTests.cs @@ -39,7 +39,7 @@ await TestConversionCSharpToVisualBasic( public static string Text { get; private set; }; public int Count { get; private set; }; }", -@"Friend Class TestClass + @"Friend Class TestClass Private Shared _Text As String Private _Count As Integer @@ -69,7 +69,7 @@ await TestConversionCSharpToVisualBasic( @"static class TestClass { public static string Text { get; private set; } }", -@"Friend Module TestClass + @"Friend Module TestClass Private _Text As String Public Property Text As String @@ -110,24 +110,24 @@ Private answer As Integer() = {1, 2} } [Fact] - public async Task TestMethod() + public async Task TestMethodWithComments() { await TestConversionCSharpToVisualBasic( @"class TestClass { public void TestMethod(out T argument, ref T2 argument2, T3 argument3) where T : class, new where T2 : struct { - argument = null; - argument2 = default(T2); - argument3 = default(T3); + argument = null; //1 + argument2 = default(T2); //2 + argument3 = default(T3); //3 } }", @"Imports System.Runtime.InteropServices Friend Class TestClass Public Sub TestMethod(Of T As {Class, New}, T2 As Structure, T3)( ByRef argument As T, ByRef argument2 As T2, ByVal argument3 As T3) - argument = Nothing - argument2 = Nothing - argument3 = Nothing + argument = Nothing ' 1 + argument2 = Nothing ' 2 + argument3 = Nothing ' 3 End Sub End Class"); } @@ -427,7 +427,7 @@ End Sub public async Task TestExtensionMethodWithExistingImport() { await TestConversionCSharpToVisualBasic( -@"using System; +@"using System; //Gets simplified away using System.Runtime.CompilerServices; static class TestClass @@ -499,7 +499,7 @@ public int Test1 { } } ", -@"Public Class HasConflictingPropertyAndField + @"Public Class HasConflictingPropertyAndField Private f_Test As Integer Public Property Test As Integer @@ -597,7 +597,7 @@ public int Test { set { test = value} } }", -@"Public Class HasConflictingPropertyAndField + @"Public Class HasConflictingPropertyAndField Public Function HasConflictingParam(ByVal test As Integer) As Integer f_Test = test Return test @@ -630,7 +630,7 @@ public int HasConflictingParam(int test) { return test; } }", -@"Public Class HasConflictingPropertyAndField + @"Public Class HasConflictingPropertyAndField Private f_Test As Integer Public Property Test As Integer @@ -671,7 +671,7 @@ public int Test { set { test = value} } }", -@"Public Partial Class HasConflictingPropertyAndField + @"Public Partial Class HasConflictingPropertyAndField Public Function HasConflictingParam(ByVal test As Integer) As Integer Dim l_TEST As Integer = 0 f_Test = test + l_TEST @@ -885,8 +885,7 @@ public async Task TestExternDllImport() { await TestConversionCSharpToVisualBasic( @"[DllImport(""kernel32.dll"", SetLastError = true)] -static extern IntPtr OpenProcess(AccessMask dwDesiredAccess, bool bInheritHandle, uint dwProcessId);", @" - +static extern IntPtr OpenProcess(AccessMask dwDesiredAccess, bool bInheritHandle, uint dwProcessId);", @" Private Shared Function OpenProcess(ByVal dwDesiredAccess As AccessMask, ByVal bInheritHandle As Boolean, ByVal dwProcessId As UInteger) As IntPtr End Function"); } @@ -923,7 +922,7 @@ public void Reset() { backingField = null; } }", -@"Imports System + @"Imports System Friend Class TestClass Private backingField As EventHandler @@ -958,7 +957,7 @@ public event EventHandler MyEvent { remove { _backingField -= value; } } }", -@"Imports System + @"Imports System Friend Class TestClass Private _backingField As EventHandler @@ -1010,8 +1009,7 @@ End Event [Fact] public async Task SubscribeEventInPropertySetter() { await TestConversionCSharpToVisualBasic( -@"using System; -using System.ComponentModel; +@"using System.ComponentModel; class TestClass { OwnerClass owner; @@ -1128,7 +1126,7 @@ await TestConversionCSharpToVisualBasic( @"public interface iDisplay { object this[int i] { get; set; } }", -@"Public Interface iDisplay + @"Public Interface iDisplay Default Property Item(ByVal i As Integer) As Object End Interface"); } @@ -1145,7 +1143,7 @@ public object this[int index] { set { } } }", -@"Imports System.Collections + @"Imports System.Collections Friend Class TestClass Implements IList @@ -1169,7 +1167,7 @@ public object this[int index] { set { } } }", -@"Friend Class TestClass + @"Friend Class TestClass Default Public Property Item(ByVal index As Integer) As Object Get End Get @@ -1181,12 +1179,12 @@ End Property [Fact] public async Task NameMatchesWithTypeDate() { await TestConversionCSharpToVisualBasic( -@"using System; +@"using System; //Gets simplified away class TestClass { private DateTime date; }", -@"Friend Class TestClass + @"Friend Class TestClass Private [date] As Date End Class"); } @@ -1198,7 +1196,7 @@ public object TestMethod(System.Type param1, System.Globalization.CultureInfo pa return null; } }", -@"Public Class TestClass + @"Public Class TestClass Public Function TestMethod(ByVal param1 As System.Type, ByVal param2 As System.Globalization.CultureInfo) As Object Return Nothing End Function @@ -1237,7 +1235,7 @@ public override bool Equals(object obj) { return Equals((MailEmployee)obj); } }", -@"Public Class MailEmployee + @"Public Class MailEmployee Public Property Email As String Protected Overloads Function Equals(ByVal other As MailEmployee) As Boolean @@ -1255,7 +1253,7 @@ await TestConversionCSharpToVisualBasic( @"public interface IParametersProvider { IEnumerable Parameters { get; } }", -@"Public Interface IParametersProvider + @"Public Interface IParametersProvider ReadOnly Property Parameters As IEnumerable(Of Object) End Interface"); } @@ -1265,7 +1263,7 @@ await TestConversionCSharpToVisualBasic( @"public interface IParametersProvider { IEnumerable Parameters { set; } }", -@"Public Interface IParametersProvider + @"Public Interface IParametersProvider WriteOnly Property Parameters As IEnumerable(Of Object) End Interface"); } diff --git a/Tests/VB/NamespaceLevelTests.cs b/Tests/VB/NamespaceLevelTests.cs index d44072cca..e01f48925 100644 --- a/Tests/VB/NamespaceLevelTests.cs +++ b/Tests/VB/NamespaceLevelTests.cs @@ -38,9 +38,11 @@ s GetStr() return s.Empty; } }", - @"Public Class X - Private Function GetStr() As String - Return String.Empty + @"Imports s = System.String + +Public Class X + Private Function GetStr() As s + Return s.Empty End Function End Class"); } @@ -97,7 +99,7 @@ public class Test1 : @class.TestClass } } ", -@"Namespace Test.class + @"Namespace Test.class Public MustInherit Class TestClass End Class End Namespace @@ -251,7 +253,7 @@ class ThisUri private MyAlias.SoapAnyUri s; private SO so; }", - @"Imports MyAlias = System.Runtime.Remoting.Metadata.W3cXsd2001 + @"Imports MyAlias = System.Runtime.Remoting.Metadata.W3cXsd2001 Imports SO = System.Runtime.Remoting.Metadata.SoapOption Friend Class ThisUri @@ -264,7 +266,7 @@ Private so As SO public async Task MoveImportsStatement() { await TestConversionCSharpToVisualBasic("namespace test { using SomeNamespace; }", - @"Imports SomeNamespace + @"Imports SomeNamespace Namespace test End Namespace"); @@ -275,13 +277,12 @@ public async Task InnerNamespace_MoveImportsStatement() { await TestConversionCSharpToVisualBasic( @"namespace System { - using Collections; + using Collections; // Moves outside namespace public class TestClass { public Hashtable Property { get; set; } } }", -@"Imports System.Collections - + @"Imports System.Collections ' Moves outside namespace Namespace System Public Class TestClass Public Property [Property] As Hashtable @@ -306,7 +307,7 @@ public interface iDisplay string Name { get; set; } void DisplayName(); }", -@"Public Class ToBeDisplayed + @"Public Class ToBeDisplayed Implements iDisplay Public Property Name As String Implements iDisplay.Name @@ -338,7 +339,7 @@ public interface iDisplay string Name { get; set; } void DisplayName(); }", -@"Public Class ToBeDisplayed + @"Public Class ToBeDisplayed Implements iDisplay Private Property Name As String Implements iDisplay.Name @@ -366,7 +367,7 @@ object iDisplay.this[int i] { public interface iDisplay { object this[int i] { get; set; } }", -@"Public Class ToBeDisplayed + @"Public Class ToBeDisplayed Implements iDisplay Private Property Item(ByVal i As Integer) As Object Implements iDisplay.Item @@ -388,7 +389,7 @@ Default Property Item(ByVal i As Integer) As Object public async Task ClassImplementsInterface2() { await TestConversionCSharpToVisualBasic("class test : System.IComparable { }", -@"Friend Class test + @"Friend Class test Implements IComparable End Class"); } @@ -397,7 +398,7 @@ Implements IComparable public async Task ClassInheritsClass() { await TestConversionCSharpToVisualBasic("using System.IO; class test : InvalidDataException { }", -@"Imports System.IO + @"Imports System.IO Friend Class test Inherits InvalidDataException @@ -408,7 +409,7 @@ Inherits InvalidDataException public async Task ClassInheritsClass2() { await TestConversionCSharpToVisualBasic("class test : System.IO.InvalidDataException { }", -@"Friend Class test + @"Friend Class test Inherits InvalidDataException End Class"); } @@ -429,7 +430,7 @@ public static Task Method() { return task; } }", -@"Imports System.Threading.Tasks + @"Imports System.Threading.Tasks Public MustInherit Class Class1 End Class @@ -454,7 +455,7 @@ static class Generator { public static void Initialize() { } } }", -@"Public Module Factory + @"Public Module Factory Friend NotInheritable Class Generator Public Shared Sub Initialize() End Sub @@ -472,7 +473,7 @@ public static void Initialize() { } internal static void Initialize1() { } private static void Initialize2() { } }", -@"Public Module Factory + @"Public Module Factory Private Const Name As String = ""a"" Friend Const Name1 As String = ""b"" Public Const Name2 As String = ""c"" @@ -497,8 +498,8 @@ await TestConversionCSharpToVisualBasic( public class TestClass : ITestInterface { public void Method(List list) { } -", -@"Public Interface ITestInterface(Of T) +}", + @"Public Interface ITestInterface(Of T) Sub Method(ByVal list As List(Of T)) End Interface @@ -515,8 +516,8 @@ await TestConversionCSharpToVisualBasic( @"using System.ComponentModel; public class TestClass : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; -", -@"Imports System.ComponentModel +}", + @"Imports System.ComponentModel Public Class TestClass Implements INotifyPropertyChanged @@ -529,7 +530,7 @@ public async Task FullQualificationInImplements() { await TestConversionCSharpToVisualBasic( @"public class TestClass : System.ComponentModel.INotifyPropertyChanged { public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; -", +}", @"Public Class TestClass Implements System.ComponentModel.INotifyPropertyChanged diff --git a/Tests/VB/SpecialConversionTests.cs b/Tests/VB/SpecialConversionTests.cs index d7238dc4c..e40c1d93c 100644 --- a/Tests/VB/SpecialConversionTests.cs +++ b/Tests/VB/SpecialConversionTests.cs @@ -372,7 +372,7 @@ await TestConversionCSharpToVisualBasic( object aB = 5; int Ab = (int) o; }", -@"Private Sub Test() + @"Private Sub Test() Dim l_AB As Object = 5 Dim Ab As Integer = CInt(o) End Sub"); @@ -384,7 +384,7 @@ await TestConversionCSharpToVisualBasic( object test = 5; int tesT = (int) o; }", -@"Private Sub Test() + @"Private Sub Test() Dim l_Test1 As Object = 5 Dim l_TesT As Integer = CInt(o) End Sub"); @@ -399,7 +399,7 @@ await TestConversionCSharpToVisualBasic( return test; } }", -@"Public ReadOnly Property Test As Integer + @"Public ReadOnly Property Test As Integer Get Dim l_Test1 As Object = 5 Dim l_TesT As Integer = CInt(o) @@ -427,7 +427,7 @@ public event System.EventHandler Test { } } }", -@"Friend Class TestClass + @"Friend Class TestClass Private f_Test As EventHandler Public Custom Event Test As EventHandler @@ -454,7 +454,7 @@ await TestConversionCSharpToVisualBasic( int tesT = (int)test; return tesT; }", -@"Private Function Method(ByVal test As Object) As Integer + @"Private Function Method(ByVal test As Object) As Integer Dim l_TesT As Integer = test Return l_TesT End Function"); @@ -468,7 +468,7 @@ public class TestClass { public int Test { get { return test; } } } }", -@"Namespace System + @"Namespace System Public Class TestClass Private f_Test As Integer diff --git a/Tests/VB/StandaloneStatementTests.cs b/Tests/VB/StandaloneStatementTests.cs index 2d58210ee..85d6a367a 100644 --- a/Tests/VB/StandaloneStatementTests.cs +++ b/Tests/VB/StandaloneStatementTests.cs @@ -58,7 +58,7 @@ public async Task SingleAssigment() { await TestConversionCSharpToVisualBasic( @"var x = 3;", -@"Dim x = 3"); + @"Dim x = 3"); } [Fact] @@ -66,7 +66,7 @@ public async Task SingleFieldDeclaration() { await TestConversionCSharpToVisualBasic( @"private int x = 3;", -@"Private x As Integer = 3"); + @"Private x As Integer = 3"); } [Fact] @@ -76,7 +76,7 @@ await TestConversionCSharpToVisualBasic( @"public class Test { }", -@"Public Class Test + @"Public Class Test End Class"); } @@ -85,7 +85,7 @@ public async Task SingleAbstractMethod() { await TestConversionCSharpToVisualBasic( @"protected abstract void abs();", -@"Protected MustOverride Sub abs()"); + @"Protected MustOverride Sub abs()"); } [Fact] @@ -95,7 +95,7 @@ await TestConversionCSharpToVisualBasic( @"namespace nam { }", -@"Namespace nam + @"Namespace nam End Namespace"); } @@ -106,7 +106,7 @@ await TestConversionCSharpToVisualBasic( @"this.DataContext = from task in tasks where task.Priority == pri select task;", -@"Me.DataContext = From task In tasks Where task.Priority Is pri Select task"); + @"Me.DataContext = From task In tasks Where task.Priority Is pri Select task"); } } } diff --git a/Tests/VB/StatementTests.cs b/Tests/VB/StatementTests.cs index 915d10d91..8772866d5 100644 --- a/Tests/VB/StatementTests.cs +++ b/Tests/VB/StatementTests.cs @@ -84,22 +84,22 @@ void TestMethod(int arg) { } }", @"Friend Class TestClass Private Sub TestMethod(ByVal arg As Integer) - While True + While True ' Becomes while loop If arg = 3 Then Exit While Select Case arg - Case 1 - Case 2 + Case 1 ' From switch + Case 2 ' From switch Case Else - Continue While + Continue While ' Outer while loop End Select - For i = 0 To arg - 1 - If arg <> 1 Then Exit For - Continue For + For i = 0 To arg - 1 ' Becomes For Next loop + If arg <> 1 Then Exit For ' From inner for loop + Continue For ' Inner for loop Next - Continue While + Continue While ' Outer while loop End While End Sub End Class"); @@ -747,7 +747,7 @@ await TestConversionCSharpToVisualBasic(@"class TestClass { void TestMethod() { - for (int i = 0; unknownCondition; i++) { + for (int i = 0; unknownCondition; i++) { // Increment moves to bottom of loop b[i] = s[i]; } } @@ -757,7 +757,7 @@ Private Sub TestMethod() While unknownCondition b(i) = s(i) - i += 1 + i += 1 ' Increment moves to bottom of loop End While End Sub End Class"); @@ -831,7 +831,7 @@ void TestMethod() { } } }", -@"Friend Class TestClass + @"Friend Class TestClass Private Sub TestMethod() Dim summary As Integer = 0 @@ -855,7 +855,7 @@ void TestMethod() { void Draw(double height) { } }", -@"Friend Class TestClass + @"Friend Class TestClass Private height As Double Private Sub TestMethod() @@ -892,7 +892,7 @@ void TestMethod(IEnumerable counts) { }; } }", -@"Friend Class TestClass + @"Friend Class TestClass Private Sub TestMethod(ByVal counts As IEnumerable(Of Integer)) Dim summary As Integer = 0 Dim action As Action = Sub() @@ -1127,8 +1127,8 @@ void TestMethod(int number) Console.Write(""section 4""); goto default; default: - Console.Write(""default section""); - break; + Console.Write(""default section""); // Block moves to end - 1 + break; // Block moves to end - 2 case 5: Console.Write(""section 5""); break; @@ -1150,7 +1150,8 @@ Case 5 Console.Write(""section 5"") Case Else _Select0_CaseDefault: - Console.Write(""default section"") + Console.Write(""default section"") ' Block moves to end - 1 + ' Block moves to end - 2 End Select End Sub End Class"); diff --git a/Tests/VB/TriviaTests.cs b/Tests/VB/TriviaTests.cs new file mode 100644 index 000000000..8eb0263f3 --- /dev/null +++ b/Tests/VB/TriviaTests.cs @@ -0,0 +1,105 @@ +using System.Threading.Tasks; +using CodeConverter.Tests.TestRunners; +using Xunit; + +namespace CodeConverter.Tests.VB +{ + public class TriviaTests : ConverterTestBase + { + [Fact] + public async Task MethodWithComments() + { + await TestConversionCSharpToVisualBasic( + @"using System; +using System.Diagnostics; //Using statement + +//blank line + +namespace ANamespace //namespace +{ // Block start - namespace + /// + /// class xml doc + /// + class CommentTestClass //Don't rename + { //Keep this method at the top + /// + /// method xml doc + /// + public void TestMethod(out T argument, ref T2 argument2, T3 argument3) where T : class where T2 : struct //Only for structs + { // Block start - method + #if true //BUG: IfDirective loses comments + argument = null; //1 + #region Arg2 + argument2 = default(T2); //2 + #endregion // EndRegion loses comments + if (argument != null) //never + { // Block start - if + // leading trivia for the next line + Debug.WriteLine(1); // Check debug window + Debug.WriteLine(2); + } //argument1 != null + argument3 = default(T3); //3 + #else //BUG: ElseDirective loses comments + argument = new object(); + #endif //BUG: EndIfDirective loses comments + Console.Write(3); + } //End of method + } //End of class +} +// Last line comment", @"Imports System +Imports System.Diagnostics ' Using statement +Imports System.Runtime.InteropServices + + +' blank line + +Namespace ANamespace ' namespace + ' Block start - namespace + ''' + ''' class xml doc + ''' + Friend Class CommentTestClass ' Don't rename + ' Keep this method at the top + ''' + ''' method xml doc + ''' + Public Sub TestMethod(Of T As Class, T2 As Structure, T3)( ByRef argument As T, ByRef argument2 As T2, ByVal argument3 As T3) ' Only for structs + ' Block start - method +#If true + argument = Nothing ' 1 +#Region ""Arg2"" + argument2 = Nothing ' 2 +#End Region + + If argument IsNot Nothing Then ' never + ' Block start - if + ' leading trivia for the next line + Debug.WriteLine(1) ' Check debug window + Debug.WriteLine(2) + End If ' argument1 != null + argument3 = Nothing ' 3 +#Else +' Skipped during conversion: argument = new object(); +#End If + Console.Write(3) + End Sub ' End of method + End Class ' End of class +End Namespace' Last line comment"); + } + + [Fact] + public async Task TrailingAndEndOfFileLineComments() + { + await TestConversionCSharpToVisualBasic( + @"//leading +namespace ANamespace //namespace +{ // start of block - namespace +} //end namespace +// Last line comment", @"' leading +Namespace ANamespace ' namespace + ' start of block - namespace +End Namespace ' end namespace +' Last line comment"); + } + } +} diff --git a/Tests/VB/TypeCastTests.cs b/Tests/VB/TypeCastTests.cs index 43458fb7a..2bf2ad05e 100644 --- a/Tests/VB/TypeCastTests.cs +++ b/Tests/VB/TypeCastTests.cs @@ -9,8 +9,10 @@ public class TypeCastTests : ConverterTestBase [Fact] public async Task CastObjectToInteger() { + // The leading and trailing newlines check that surrounding trivia is selected as part of this (in the comments auto-testing) await TestConversionCSharpToVisualBasic( - @"void Test() + @" +void Test() { object o = 5; int i = (int) o; @@ -30,12 +32,10 @@ await TestConversionCSharpToVisualBasic( { object o = ""Test""; string s = (string) o; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = ""Test"" Dim s As String = CStr(o) -End Sub -"); +End Sub"); } [Fact] @@ -46,12 +46,10 @@ await TestConversionCSharpToVisualBasic( { object o = new System.Collections.Generic.List(); System.Collections.Generic.List l = (System.Collections.Generic.List) o; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = New System.Collections.Generic.List(Of Integer)() Dim l As List(Of Integer) = CType(o, System.Collections.Generic.List(Of Integer)) -End Sub -"); +End Sub"); } [Fact] @@ -62,12 +60,10 @@ await TestConversionCSharpToVisualBasic( { object o = 5; System.Nullable i = o as int?; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = 5 Dim i As Integer? = o -End Sub -"); +End Sub"); } [Fact] @@ -78,12 +74,10 @@ await TestConversionCSharpToVisualBasic( { object o = new System.Collections.Generic.List(); System.Collections.Generic.List l = o as System.Collections.Generic.List; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = New System.Collections.Generic.List(Of Integer)() Dim l As List(Of Integer) = TryCast(o, System.Collections.Generic.List(Of Integer)) -End Sub -"); +End Sub"); } [Fact] @@ -93,11 +87,9 @@ await TestConversionCSharpToVisualBasic( @"void Test() { object o = 5L; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = 5L -End Sub -"); +End Sub"); } [Fact] @@ -107,11 +99,9 @@ await TestConversionCSharpToVisualBasic( @"void Test() { object o = 5.0f; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = 5.0F -End Sub -"); +End Sub"); } [Fact] @@ -121,11 +111,9 @@ await TestConversionCSharpToVisualBasic( @"void Test() { object o = 5.0m; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim o As Object = 5.0D -End Sub -"); +End Sub"); } [Fact] @@ -135,11 +123,9 @@ await TestConversionCSharpToVisualBasic( @"void Test() { char CR = (char)0xD; -} -", @"Private Sub Test() +}", @"Private Sub Test() Dim CR As Char = ChrW(&HD) -End Sub -"); +End Sub"); } [Fact] public async Task MethodInvocation() { @@ -152,7 +138,7 @@ public void TestMethod(object o) { ((Test)o).TestMethod(); } }", -@"Public Class Test + @"Public Class Test Public Sub TestMethod() End Sub End Class @@ -174,7 +160,7 @@ public void TestMethod(object o) { (o as Test).TestMethod(); } }", -@"Public Class Test + @"Public Class Test Public Sub TestMethod() End Sub End Class