using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using JavaAst = com.github.javaparser.ast;
using JavaComments = com.github.javaparser.ast.comments;
using JavaParser = com.github.javaparser;
using SysRegex = System.Text.RegularExpressions;
namespace JavaToCSharp
{
public static class CommentsHelper
{
private enum CommentPosition
{
Leading,
Trailing
}
// Regex: Optional *, capture optional @par, capture optional text (keep leading whitespaces, trim end).
private static readonly SysRegex.Regex _analyzeDocString =
new(@"^(\s*\*)?(\s*(?@[a-z]+))?\s?(?.*?)\s*$", SysRegex.RegexOptions.Compiled);
private static readonly Dictionary _knownTagsDict = new()
{
["@param"] = "param",
["@return"] = "returns",
["@exception"] = "exception",
["@throws"] = "exception"
};
public static TSyntax AddCommentsTrivias(TSyntax syntax, JavaAst.Node node, string commentEnding) where TSyntax : SyntaxNode
{
var comments = GatherComments(node);
if (comments.Count > 0)
{
var leadingTriviaList = new List();
var trailingTriviaList = new List();
foreach (var (comment, pos) in comments)
{
var (kind, pre, post) = GetCommentInfo(comment, commentEnding);
if (kind == SyntaxKind.XmlComment)
{
leadingTriviaList.AddRange(ConvertDocComment(comment, post));
}
else
{
var commentTrivia = SyntaxFactory.SyntaxTrivia(kind, pre + comment.getContent() + post);
if (pos == CommentPosition.Leading)
{
leadingTriviaList.Add(commentTrivia);
}
else
{
trailingTriviaList.Add(commentTrivia);
}
}
}
syntax = syntax
.WithLeadingTrivia(leadingTriviaList)
.WithTrailingTrivia(trailingTriviaList);
}
return syntax;
}
private static (SyntaxKind kind, string pre, string post) GetCommentInfo(JavaComments.Comment comment, string commentEnding)
{
return comment switch
{
JavaComments.BlockComment => (SyntaxKind.MultiLineCommentTrivia, "/*", "*/" + commentEnding),
JavaComments.JavadocComment => (SyntaxKind.XmlComment, null, commentEnding),
_ => (SyntaxKind.SingleLineCommentTrivia, "//", commentEnding),
};
}
private static List<(JavaComments.Comment c, CommentPosition pos)> GatherComments(JavaAst.Node node)
{
var result = new List<(JavaComments.Comment c, CommentPosition pos)>();
if (node == null) return result;
var parentNode = node.getParentNode();
if (parentNode is null)
{
if (node.hasComment())
{
result.Add((node.getComment(), CommentPosition.Leading));
}
}
else
{
var unsortedComments = parentNode.getAllContainedComments();
if (unsortedComments.size() != 0)
{
var comments = unsortedComments.OfType()
.OrderBy(c => c.getBegin().line)
.ThenBy(c => c.getBegin().column)
.ToList();
// Find leading comments
var nodeBegin = node.getBegin();
var previousSibling = GetPreviousSibling(parentNode, nodeBegin);
int previousPos = previousSibling?.getEnd().line ?? 0;
var leadingComments = GetLeadingComments(comments, nodeBegin, previousPos);
result.AddRange(leadingComments);
// Find trailing comments.
// We consider only comments either appearing on the same line or, if no sibling nodes follow,
// then also comments on the succeeding lines (because otherwise they belong to the next sibling).
var nodeEnd = node.getEnd();
var trailingComments = HasNextSibling(parentNode, nodeEnd)
? GetLineEndComment(comments, nodeEnd)
: GetTrailingComments(comments, nodeEnd);
result.AddRange(trailingComments);
}
}
return result;
}
private static IEnumerable<(JavaComments.Comment c, CommentPosition Trailing)> GetTrailingComments(IEnumerable comments, JavaParser.Position nodeEnd) =>
comments.Where(c =>
{
var commentBegin = c.getBegin();
return commentBegin.line == nodeEnd.line && commentBegin.column > nodeEnd.column || commentBegin.line > nodeEnd.line;
})
.Select(c => (c, CommentPosition.Trailing));
private static IEnumerable<(JavaComments.Comment c, CommentPosition Trailing)> GetLineEndComment(IEnumerable comments, JavaParser.Position nodeEnd) =>
comments
.Where(c => c.getBegin().line == nodeEnd.line && c.getBegin().column > nodeEnd.column)
.Select(c => (c, CommentPosition.Trailing));
private static bool HasNextSibling(JavaAst.Node parentNode, JavaParser.Position nodeEnd)
{
return parentNode.getChildrenNodes()
.OfType()
.Where(sibling => sibling is not JavaComments.Comment)
.Any(sibling =>
{
var siblingBegin = sibling.getBegin();
return siblingBegin.line > nodeEnd.line || siblingBegin.line == nodeEnd.line && siblingBegin.column > nodeEnd.column;
});
}
private static IEnumerable<(JavaComments.Comment c, CommentPosition Leading)> GetLeadingComments(IEnumerable comments, JavaParser.Position nodeBegin, int previousPos)
{
return comments.Where(c =>
{
var commentEnd = c.getEnd();
return c.getBegin().line > previousPos && (commentEnd.line < nodeBegin.line || commentEnd.line == nodeBegin.line && commentEnd.column < nodeBegin.column);
})
.Select(c => (c, CommentPosition.Leading));
}
private static JavaAst.Node GetPreviousSibling(JavaAst.Node parentNode, JavaParser.Position nodeBegin)
{
return parentNode.getChildrenNodes()
.OfType()
.Where(sibling => sibling is not JavaComments.Comment)
.LastOrDefault(sibling =>
{
var siblingEnd = sibling.getEnd();
return siblingEnd.line < nodeBegin.line || siblingEnd.line == nodeBegin.line && siblingEnd.column < nodeBegin.column;
});
}
///
/// Convert `JavaAst.Node` code to Comments
///
///
///
///
///
public static IEnumerable ConvertToComment(IEnumerable codes, string tag, bool hasBlockMark = true)
{
var outputs = new List();
foreach (var code in codes)
{
string[] input = code.ToString().Split(new[] { "\r\n" }, StringSplitOptions.None);
outputs.AddRange(input);
}
if (outputs.Count > 0)
{
if (hasBlockMark)
{
yield return SyntaxFactory.Comment($"\r\n");
yield return SyntaxFactory.Comment($"// --------------------");
yield return SyntaxFactory.Comment($"// TODO {tag}");
}
foreach (var t in outputs)
{
yield return SyntaxFactory.Comment($"// {t}");
}
if (hasBlockMark)
yield return SyntaxFactory.Comment($"// --------------------");
}
}
private static IEnumerable ConvertDocComment(JavaComments.Comment comment, string post)
{
string[] input = comment.getContent().Split(new[] { "\r\n" }, StringSplitOptions.None);
var output = new List();
var remarks = new List(); // For Java tags unknown in C#
var currentOutput = output;
string tag = null;
foreach (string inputLine in input)
{
var match = _analyzeDocString.Match(inputLine);
if (match.Success)
{
string paramName = match.Groups["param"].Value;
string text = match.Groups["text"].Value;
if (_knownTagsDict.TryGetValue(paramName, out var newTag))
{
CloseSection(output, tag);
tag = newTag;
currentOutput = output;
OpenSection(output, tag, text);
}
else if (paramName.Length > 0)
{
// Add other parameters to remarks section.
CloseSection(output, tag);
currentOutput = remarks;
remarks.Add(paramName + text);
tag = "remarks";
}
else if (tag == null)
{
tag = "summary";
OpenSection(output, tag, text);
}
else
{
currentOutput.Add(text); // Add additional text lines to the same section.
}
}
}
CloseSection(output, tag);
AppendRemarks(output, remarks);
if (output.Count > 0)
{
output[output.Count - 1] += post;
foreach (var t in output)
{
yield return SyntaxFactory.Comment("/// " + t);
}
}
}
private static void AppendRemarks(List output, IList remarks)
{
TrimTrailingEmptyLines(remarks);
if (remarks.Count == 1)
{
remarks[0] = $"{remarks[0]}";
}
else if (remarks.Count > 1)
{
remarks.Insert(0, "");
remarks.Add("");
}
output.AddRange(remarks);
}
private static void CloseSection(IList output, string tag)
{
if (output.Count > 0 && tag != "remarks")
{
TrimTrailingEmptyLines(output);
string xmlEndTag = $"{tag}>";
if (tag == "summary")
{
// Summary tags are always on separate lines.
output.Add(xmlEndTag);
}
else
{
output[output.Count - 1] += xmlEndTag;
}
}
}
private static void TrimTrailingEmptyLines(IList lines)
{
while (lines.Count > 0 && lines[lines.Count - 1].Trim() == "")
{
lines.RemoveAt(lines.Count - 1);
}
}
private static void OpenSection(ICollection output, string tag, string text)
{
string id, label;
switch (tag)
{
case "summary":
output.Add("");
if (text.Trim() != "")
{
// Do not include the first empty line.
output.Add(text);
}
break;
case "param": // label
(id, label) = ParseByFirstWord(text);
output.Add($"{label}");
break;
case "exception": // label
(id, label) = ParseByFirstWord(text);
output.Add($"{label}");
break;
default:
output.Add($"<{tag}>{text}");
break;
}
static (string id, string label) ParseByFirstWord(string text)
{
string id = text.Split()[0];
string label = text.Substring(id.Length).TrimStart();
return (id, label);
}
}
public static SyntaxNode FixCommentsWhitespaces(SyntaxNode node)
{
// Comments are inserted before whitespace normalization. The following fixes must be executed after
// whitespace normalization to be effective.
if (node.HasLeadingTrivia)
{
node = InsertEmptyLineBeforeComment(node);
node = AdjustBlockCommentIndentation(node);
}
return node;
static SyntaxNode InsertEmptyLineBeforeComment(SyntaxNode node)
{
/* For increased readability we change this
*
* DoSomething();
* // Comment
* DoSomethingElse();
*
* to this
*
* DoSomething();
*
* // Comment
* DoSomethingElse();
*/
if (node is StatementSyntax statement)
{
var leading = node.GetLeadingTrivia();
var index = leading.IndexOf(SyntaxKind.SingleLineCommentTrivia);
if (index >= 0)
{
if (index > 0 && leading[index - 1].Kind() == SyntaxKind.WhitespaceTrivia)
{
index--;
}
node = statement.InsertTriviaBefore(leading[index],
Enumerable.Repeat(SyntaxFactory.CarriageReturnLineFeed, 1));
}
}
return node;
}
}
private static SyntaxNode AdjustBlockCommentIndentation(SyntaxNode node)
{
// The first line of multiline comments is adjusted by the whitespace normalization but not the
// following lines.
var leading = node.GetLeadingTrivia();
for (int i = 0; i < leading.Count; i++)
{
var t = leading[i];
if (t.Kind() == SyntaxKind.MultiLineCommentTrivia)
{
int indentation = GetIndentation(leading, i) + 1; // Add one to align stars.
string[] lines = t.ToFullString().Split(new[] { "\r\n" }, StringSplitOptions.None);
string indentString = new(' ', indentation);
for (int l = 1; l < lines.Length; l++)
{
// Skip first line with "/*"
lines[l] = indentString + lines[l].TrimStart();
}
node = node.ReplaceTrivia(t, SyntaxFactory.Comment(string.Join("\r\n", lines).TrimEnd(' ')));
}
}
return node;
}
private static int GetIndentation(SyntaxTriviaList leading, int commentIndex)
{
SyntaxTrivia whiteSpaceTrivia;
if (commentIndex > 0 && leading[commentIndex - 1].Kind() == SyntaxKind.WhitespaceTrivia)
{
// Try to get the indentation from the whitespace leading the comment.
whiteSpaceTrivia = leading[commentIndex - 1];
}
else if (leading.Last().Kind() == SyntaxKind.WhitespaceTrivia)
{
// Try to get the indentation of the node from the last leading trivia.
whiteSpaceTrivia = leading.Last();
}
else
{
return 0;
}
string s = whiteSpaceTrivia.ToFullString().Replace("\t", " ");
return s.All(c => c == ' ') ? s.Length : 0;
}
}
}