Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
db2b431
Easy fixups
GrahamTheCoder Jan 25, 2020
c6a4391
Improve todo messages
GrahamTheCoder Jan 25, 2020
01123d3
Be explicit about language
GrahamTheCoder Jan 25, 2020
4f46104
Allow regions
GrahamTheCoder Jan 26, 2020
e12ff8e
Map line-by-line trivia where expressions are close by
GrahamTheCoder Jan 21, 2020
b9d26c0
Implement rudimentary trivia conversions
GrahamTheCoder Jan 26, 2020
e725937
Recharacterize - not sure why assembly attributes have gained newline
GrahamTheCoder Jan 26, 2020
e9b3b88
Remove extra newlines in tests
GrahamTheCoder Jan 26, 2020
8e5c460
Add mapping for identifiers (so the start of blocks works better)
GrahamTheCoder Jan 26, 2020
bbe0089
Split class, separate leading/trailing and annotate more nodes
GrahamTheCoder Jan 27, 2020
5129242
Test comments automatically
GrahamTheCoder Jan 28, 2020
8fe5a03
Add information useful for debug that could be used to split phase
GrahamTheCoder Jan 28, 2020
de03b54
Remove guessing phase
GrahamTheCoder Jan 28, 2020
31671dd
Off topic: This test wasn't inverted after being copied
GrahamTheCoder Jan 29, 2020
f7d0efc
Test that passes but characterizes some remaining issues
GrahamTheCoder Jan 29, 2020
a8372c9
Fix missing quotes for region name
GrahamTheCoder Jan 29, 2020
2f6e380
Fix compilation of test case
GrahamTheCoder Jan 29, 2020
cf7f0af
Avoid duplication and increase happy path performance
GrahamTheCoder Jan 29, 2020
ef1f5e7
Improve errors messages for comment bugs
GrahamTheCoder Jan 29, 2020
2e7f1d0
Remove unused
GrahamTheCoder Jan 29, 2020
11138c4
Deal with some zero width tokens like EOF
GrahamTheCoder Jan 29, 2020
c91bf71
Start trying to deal with block starts
GrahamTheCoder Jan 30, 2020
2b4219f
Improve assertions
GrahamTheCoder Jan 30, 2020
f94e7d9
Cater for block starts with carry over and recharacterize (31 fail)
GrahamTheCoder Jan 31, 2020
cb7a97f
Comment simplified line
GrahamTheCoder Jan 31, 2020
ab16d70
Zero index for easier debugging of the mapper
GrahamTheCoder Jan 31, 2020
4d184e9
Add indirection similar to what's used in VB-> C#
GrahamTheCoder Jan 31, 2020
62cec21
Don't map trivia for these nodes
GrahamTheCoder Jan 31, 2020
6f00fb1
Cover descendants too for future sanity (23 tests fail)
GrahamTheCoder Jan 31, 2020
430507e
Comment intentionally moved using statement (22 tests fail)
GrahamTheCoder Jan 31, 2020
c3f7396
Manually map block ends where they might end a file
GrahamTheCoder Feb 1, 2020
47f9faf
Never find zero width tokens (24 tests fail)
GrahamTheCoder Feb 1, 2020
99dc501
Use elastic crlf but don't normalize - recharectirize
GrahamTheCoder Feb 1, 2020
2dba16f
Change order
GrahamTheCoder Feb 1, 2020
d1ba056
Use collections
GrahamTheCoder Feb 1, 2020
9e0ebc8
Deal with using directives better
GrahamTheCoder Feb 1, 2020
ec8799a
renames
GrahamTheCoder Feb 2, 2020
0028651
Add new members
GrahamTheCoder Feb 2, 2020
e84508b
Add both systems in parallel
GrahamTheCoder Feb 2, 2020
e9a0308
Port at end - 17 fail
GrahamTheCoder Feb 2, 2020
562223c
Stop keeping track of replaced target
GrahamTheCoder Feb 2, 2020
2ad9529
Stop mutating local target parameter
GrahamTheCoder Feb 2, 2020
da09e0f
Rename
GrahamTheCoder Feb 2, 2020
4118fb7
Inline
GrahamTheCoder Feb 2, 2020
1312391
Put in todos for plans
GrahamTheCoder Feb 2, 2020
1ab1d17
Consolidate logic down since the split isn't that helpful
GrahamTheCoder Feb 2, 2020
37fc8db
Fix typo - 11 fail
GrahamTheCoder Feb 2, 2020
d35f629
Recharacterize C#
GrahamTheCoder Feb 2, 2020
eba86eb
Split accumulating trivia from placing. Pick only within source line.
GrahamTheCoder Feb 2, 2020
788f0d8
Comment moved block - 7 fail
GrahamTheCoder Feb 2, 2020
32a18ba
Comment this moved expression
GrahamTheCoder Feb 3, 2020
b161b72
SelectMany and other tidyups
GrahamTheCoder Feb 3, 2020
d61ad15
Rename to admit these are cs functions
GrahamTheCoder Feb 3, 2020
63c2f29
Balance trivia - 5 tests fail
GrahamTheCoder Feb 3, 2020
669b20f
Tidy up
GrahamTheCoder Feb 3, 2020
1bc0ec0
Fall back on alternate dictionary
GrahamTheCoder Feb 3, 2020
7b6d0c3
Formatting/naming
GrahamTheCoder Feb 3, 2020
21c37c2
Fix disabling of source map where it isn't the original token position
GrahamTheCoder Feb 3, 2020
aa49d3c
Note limitation of this approach
GrahamTheCoder Feb 3, 2020
a057032
Remove bits that don't make any tests fail
GrahamTheCoder Feb 3, 2020
f259f76
Merge remote-tracking branch 'origin/master' into rewrite-method-trivia
GrahamTheCoder Feb 3, 2020
a1ff5b6
Remove unused using
GrahamTheCoder Feb 3, 2020
176ca5e
Characterize comments
GrahamTheCoder Feb 3, 2020
f8475a9
Revert accidental behaviour change
GrahamTheCoder Feb 3, 2020
7831c0d
Fix broken input
GrahamTheCoder Feb 3, 2020
5c66b33
Revert unncessary changes
GrahamTheCoder Feb 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion ICSharpCode.CodeConverter/Shared/AnnotationConstants.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
165 changes: 165 additions & 0 deletions ICSharpCode.CodeConverter/Shared/LineTriviaMapper.cs
Original file line number Diff line number Diff line change
@@ -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<int, TextLine> _targetLeadingTextLineFromSourceLine;
private readonly IReadOnlyDictionary<int, TextLine> _targetTrailingTextLineFromSourceLine;
private readonly List<SyntaxTriviaList> _leadingTriviaCarriedOver = new List<SyntaxTriviaList>();
private readonly List<SyntaxTriviaList> _trailingTriviaCarriedOver = new List<SyntaxTriviaList>();
private readonly Dictionary<SyntaxToken, (List<IReadOnlyCollection<SyntaxTrivia>> Leading, List<IReadOnlyCollection<SyntaxTrivia>> Trailing)> _targetTokenToTrivia = new Dictionary<SyntaxToken, (List<IReadOnlyCollection<SyntaxTrivia>>, List<IReadOnlyCollection<SyntaxTrivia>>)>();

public LineTriviaMapper(SyntaxNode source, TextLineCollection sourceLines, SyntaxNode target, Dictionary<int, TextLine> targetLeadingTextLineFromSourceLine, Dictionary<int, TextLine> targetTrailingTextLineFromSourceLine)
{
_source = source;
_sourceLines = sourceLines;
_target = target;
_targetLeadingTextLineFromSourceLine = targetLeadingTextLineFromSourceLine;
_targetTrailingTextLineFromSourceLine = targetTrailingTextLineFromSourceLine;
}

/// <summary>
/// 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 */)
/// </summary>
public static SyntaxNode MapSourceTriviaToTarget<TSource, TTarget>(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();
}

/// <remarks>
/// Possible future improvements:
/// * Performance: Probably faster to find tokens starting from position of last replaced token rather than from the root node each time
/// </remarks>
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);
}

/// <summary>
/// Trailing trivia can't contain multiple newlines (it gets lost during formatting), so prepend as leading trivia of the next token
/// </summary>
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<int, TextLine> sourceToTargetLine, int sourceLineIndex)
{
if (sourceToTargetLine.TryGetValue(sourceLineIndex, out var targetLine)) return targetLine;
return default;
}

private (List<IReadOnlyCollection<SyntaxTrivia>> Leading, List<IReadOnlyCollection<SyntaxTrivia>> Trailing) GetTargetTriviaCollection(SyntaxToken toReplace)
{
if (!_targetTokenToTrivia.TryGetValue(toReplace, out var targetTrivia)) {
targetTrivia = (new List<IReadOnlyCollection<SyntaxTrivia>>(), new List<IReadOnlyCollection<SyntaxTrivia>>());
_targetTokenToTrivia[toReplace] = targetTrivia;
}

return targetTrivia;
}
}
}
4 changes: 4 additions & 0 deletions ICSharpCode.CodeConverter/Shared/ProjectConversion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ private static async Task<IEnumerable<ConversionResult>> 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);
Expand Down
56 changes: 56 additions & 0 deletions ICSharpCode.CodeConverter/Shared/TextLineExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
74 changes: 74 additions & 0 deletions ICSharpCode.CodeConverter/Shared/TriviaMapping.cs
Original file line number Diff line number Diff line change
@@ -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}";
}
}
}
Loading