Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions rewrite-csharp/csharp/OpenRewrite/CSharp/Rpc/CSharpReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,31 @@ public CSharpReceiverDelegate(CSharpReceiver outer)
public new void ConsumePreVisit(J j, RpcReceiveQueue q) => base.ConsumePreVisit(j, q);
public new void PopPreVisit() => base.PopPreVisit();

public override Space VisitSpace(Space space, RpcReceiveQueue q)
{
var comments = q.ReceiveList(space.Comments, c =>
{
// The Java side decomposes a structured CsDocComment.DocComment tree; the C#
// side has no such model, so drain it and re-flatten to a raw XmlDocComment.
if (c is StructuredDocComment)
{
return CsDocCommentReceiver.ReceiveDocComment(q);
}
var multiline = q.Receive(c.Multiline);
var text = q.Receive(c.Text);
var suffix = q.Receive(c.Suffix);
// C# Comment doesn't have Markers; consume and discard
q.Receive<Markers>(Markers.Empty);
if (c is XmlDocComment)
{
return new XmlDocComment(text!, suffix!, multiline);
}
return new TextComment(text!, suffix!, multiline);
});
var whitespace = q.Receive(space.Whitespace);
return space.WithComments(comments!).WithWhitespace(whitespace!);
}

public override J? Visit(Tree? tree, RpcReceiveQueue q)
{
// DeconstructionPattern is a J type whose visit method lives in JavaReceiver,
Expand Down
213 changes: 213 additions & 0 deletions rewrite-csharp/csharp/OpenRewrite/CSharp/Rpc/CsDocCommentReceiver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.Text;
using OpenRewrite.Core;
using OpenRewrite.Core.Rpc;
using OpenRewrite.Java;

namespace OpenRewrite.CSharp.Rpc;

/// <summary>
/// The Java side models a C# XML documentation comment as a structured
/// <c>CsDocComment.DocComment</c> tree and decomposes it over RPC. The C# side has no
/// equivalent structured model — it stores doc comments as raw <see cref="XmlDocComment"/>
/// text — so this receiver consumes the decomposed tree and re-flattens it to the textual
/// form, exactly mirroring the Java <c>CsDocCommentPrinter</c>.
/// </summary>
internal static class CsDocCommentReceiver
{
/// <summary>
/// Consume a decomposed <c>CsDocComment.DocComment</c> (id, markers, body, suffix) and
/// rebuild the raw <see cref="XmlDocComment"/>. The shell ADD message has already been
/// consumed by the caller, so the queue is positioned at the first field (id).
/// </summary>
public static XmlDocComment ReceiveDocComment(RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
string body = ReceiveNodeList(q);
string? suffix = q.Receive<string>(null);
// The printer emits "///" then the body; XmlDocComment.Text is everything after the
// leading "//", i.e. a single "/" followed by the printed body.
return new XmlDocComment("/" + body, suffix ?? "", true);
}

private static void ConsumeId(RpcReceiveQueue q) => q.Receive<object?>(null);

/// <summary>Concatenate the printed text of a (non-null) node list field.</summary>
private static string ReceiveNodeList(RpcReceiveQueue q)
{
var nodes = q.ReceiveList<DocNode>(new List<DocNode>(), null);
if (nodes == null)
{
return "";
}
var sb = new StringBuilder();
foreach (var node in nodes)
{
sb.Append(node.Text);
}
return sb.ToString();
}

/// <summary>
/// Receive a nullable node list field, returning whether it was non-null, its element
/// count, and its concatenated text. The list is always consumed to keep the queue in sync.
/// </summary>
private static (bool present, int count, string text) ReceiveNodeListNullable(RpcReceiveQueue q)
{
var nodes = q.ReceiveList<DocNode>(new List<DocNode>(), null);
if (nodes == null)
{
return (false, 0, "");
}
var sb = new StringBuilder();
foreach (var node in nodes)
{
sb.Append(node.Text);
}
return (true, nodes.Count, sb.ToString());
}

/// <summary>Replicates the printer's attribute body: "name[ space[=value]]".</summary>
private static string ReceiveAttributeBody(RpcReceiveQueue q, string name)
{
var spaceBeforeEquals = ReceiveNodeListNullable(q);
var value = ReceiveNodeListNullable(q);
if (spaceBeforeEquals.present && spaceBeforeEquals.count > 0)
{
string equalsAndValue = value.present ? "=" + value.text : "";
return name + spaceBeforeEquals.text + equalsAndValue;
}
return name;
}

// ---- Sentinel node types: not part of the C# LST, used only to drain the decomposed
// tree and rebuild text. Each implements IRpcCodec so the receive queue dispatches
// to RpcReceive. They are intentionally NOT Tree types. ----

internal abstract class DocNode : IRpcCodec<DocNode>
{
public string Text = "";

public void RpcSend(DocNode after, RpcSendQueue q) =>
throw new NotSupportedException("C# never sends structured doc comments");

public abstract DocNode RpcReceive(DocNode before, RpcReceiveQueue q);
}

internal sealed class DocXmlText : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
Text = q.Receive<string>(null) ?? "";
return this;
}
}

internal sealed class DocLineBreak : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
// Field order on the Java side is id, margin, markers.
ConsumeId(q);
Text = q.Receive<string>(null) ?? "";
q.Receive<Markers>(Markers.Empty);
return this;
}
}

internal sealed class DocXmlElement : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
string name = q.Receive<string>(null) ?? "";
string attributes = ReceiveNodeList(q);
string spaceBeforeClose = ReceiveNodeList(q);
string content = ReceiveNodeList(q);
string closingTagSpaceBeforeClose = ReceiveNodeList(q);
Text = "<" + name + attributes + spaceBeforeClose + ">" +
content +
"</" + name + closingTagSpaceBeforeClose + ">";
return this;
}
}

internal sealed class DocXmlEmptyElement : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
string name = q.Receive<string>(null) ?? "";
string attributes = ReceiveNodeList(q);
string spaceBeforeSlashClose = ReceiveNodeList(q);
Text = "<" + name + attributes + spaceBeforeSlashClose + "/>";
return this;
}
}

internal sealed class DocXmlAttribute : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
string name = q.Receive<string>(null) ?? "";
Text = ReceiveAttributeBody(q, name);
return this;
}
}

internal sealed class DocXmlCrefAttribute : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
Text = ReceiveAttributeBody(q, "cref");
// Consume the optional type-attributed J reference.
q.Receive<J?>(null);
return this;
}
}

internal sealed class DocXmlNameAttribute : DocNode
{
public override DocNode RpcReceive(DocNode before, RpcReceiveQueue q)
{
ConsumeId(q);
q.Receive<Markers>(Markers.Empty);
Text = ReceiveAttributeBody(q, "name");
// Consume the optional bound parameter/type-parameter reference.
q.Receive<J?>(null);
return this;
}
}
}

/// <summary>
/// Sentinel <see cref="Comment"/> subtype used purely so the receive queue can instantiate a
/// shell when it encounters a decomposed <c>CsDocComment.DocComment</c>. It never appears in a
/// finished LST — <see cref="CsDocCommentReceiver.ReceiveDocComment"/> replaces it with an
/// <see cref="XmlDocComment"/>.
/// </summary>
internal sealed class StructuredDocComment() : Comment("", "", true);
20 changes: 20 additions & 0 deletions rewrite-csharp/csharp/OpenRewrite/Core/Rpc/RpcReceiveQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,26 @@ private static bool IsTreeType(object obj)
"org.openrewrite.java.tree.J$VariableDeclarations$NamedVariable" =>
typeof(NamedVariable),

// Structured XML doc-comment tree (Java-only model). The C# side has no
// equivalent, so these map to sentinel shells that CsDocCommentReceiver drains
// and re-flattens into a raw XmlDocComment.
"org.openrewrite.csharp.tree.CsDocComment$DocComment" =>
typeof(OpenRewrite.CSharp.Rpc.StructuredDocComment),
"org.openrewrite.csharp.tree.CsDocComment$XmlElement" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocXmlElement),
"org.openrewrite.csharp.tree.CsDocComment$XmlEmptyElement" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocXmlEmptyElement),
"org.openrewrite.csharp.tree.CsDocComment$XmlText" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocXmlText),
"org.openrewrite.csharp.tree.CsDocComment$XmlAttribute" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocXmlAttribute),
"org.openrewrite.csharp.tree.CsDocComment$XmlCrefAttribute" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocXmlCrefAttribute),
"org.openrewrite.csharp.tree.CsDocComment$XmlNameAttribute" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocXmlNameAttribute),
"org.openrewrite.csharp.tree.CsDocComment$LineBreak" =>
typeof(OpenRewrite.CSharp.Rpc.CsDocCommentReceiver.DocLineBreak),

// Marker type overrides
"org.openrewrite.java.marker.Semicolon" =>
typeof(Semicolon),
Expand Down
6 changes: 3 additions & 3 deletions rewrite-csharp/csharp/OpenRewrite/OpenRewrite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
<PackageReference Include="NuGet.Frameworks" Version="6.12.4" />
<PackageReference Include="NuGet.Versioning" Version="6.12.4" />
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" PrivateAssets="none" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" PrivateAssets="none" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" PrivateAssets="none" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" PrivateAssets="none" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.3.0" PrivateAssets="none" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.3.0" PrivateAssets="none" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.7.8" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.openrewrite.Tree;
import org.openrewrite.csharp.CSharpVisitor;
import org.openrewrite.csharp.tree.Cs;
import org.openrewrite.csharp.tree.CsDocComment;
import org.openrewrite.csharp.tree.CsDocCommentRawComment;
import org.openrewrite.csharp.tree.Linq;
import org.openrewrite.java.internal.rpc.JavaReceiver;
Expand Down Expand Up @@ -885,6 +886,9 @@ public CSharpReceiverDelegate(CSharpReceiver delegate) {
public Space visitSpace(Space space, RpcReceiveQueue q) {
return space
.withComments(q.receiveList(space.getComments(), c -> {
if (c instanceof CsDocComment.DocComment) {
return (Comment) new CsDocCommentReceiver(delegate).visit((CsDocComment.DocComment) c, q);
}
if (c instanceof CsDocCommentRawComment) {
CsDocCommentRawComment dc = (CsDocCommentRawComment) c;
q.receive(dc.isMultiline()); // consume; always true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@
import org.openrewrite.Tree;
import org.openrewrite.csharp.CSharpVisitor;
import org.openrewrite.csharp.tree.Cs;
import org.openrewrite.csharp.tree.CsDocComment;
import org.openrewrite.csharp.tree.CsDocCommentRawComment;
import org.openrewrite.csharp.tree.Linq;
import org.openrewrite.java.internal.rpc.JavaSender;
import org.openrewrite.java.tree.*;
import org.openrewrite.rpc.RpcSendQueue;

import java.util.List;

import static org.openrewrite.rpc.Reference.asRef;
import static org.openrewrite.rpc.Reference.getValueNonNull;

Expand Down Expand Up @@ -879,23 +878,31 @@ public void visitSpace(Space space, RpcSendQueue q) {
return ((TextComment) c).getText() + c.getSuffix();
} else if (c instanceof CsDocCommentRawComment) {
return ((CsDocCommentRawComment) c).getText() + c.getSuffix();
} else if (c instanceof CsDocComment.DocComment) {
// A structured doc comment is a proper tree, so it is keyed by its id
// (like Javadoc.DocComment on the Java side) and decomposed over RPC.
return ((CsDocComment.DocComment) c).getId();
}
throw new IllegalArgumentException("Unexpected comment type " + c.getClass().getName());
},
c -> {
if (c instanceof TextComment) {
TextComment tc = (TextComment) c;
q.getAndSend(tc, TextComment::isMultiline);
q.getAndSend(tc, TextComment::getText);
} else if (c instanceof CsDocCommentRawComment) {
CsDocCommentRawComment dc = (CsDocCommentRawComment) c;
q.getAndSend(dc, CsDocCommentRawComment::isMultiline);
q.getAndSend(dc, CsDocCommentRawComment::getText);
if (c instanceof CsDocComment.DocComment) {
new CsDocCommentSender(delegate).visit((CsDocComment.DocComment) c, q);
} else {
throw new IllegalArgumentException("Unexpected comment type " + c.getClass().getName());
if (c instanceof TextComment) {
TextComment tc = (TextComment) c;
q.getAndSend(tc, TextComment::isMultiline);
q.getAndSend(tc, TextComment::getText);
} else if (c instanceof CsDocCommentRawComment) {
CsDocCommentRawComment dc = (CsDocCommentRawComment) c;
q.getAndSend(dc, CsDocCommentRawComment::isMultiline);
q.getAndSend(dc, CsDocCommentRawComment::getText);
} else {
throw new IllegalArgumentException("Unexpected comment type " + c.getClass().getName());
}
q.getAndSend(c, Comment::getSuffix);
q.getAndSend(c, Comment::getMarkers);
}
q.getAndSend(c, Comment::getSuffix);
q.getAndSend(c, Comment::getMarkers);
});
q.getAndSend(space, Space::getWhitespace);
}
Expand Down
Loading