using System.Collections.Generic; using System.Text; // The package bundles Roslyn renamed with a "UnityPipeline." prefix (Runtime/Plugins/CodeAnalysis); // the dotnet test projects reference the same DLLs (IlInterpreter/BundledRoslyn.props). using UnityPipeline.Microsoft.CodeAnalysis; using UnityPipeline.Microsoft.CodeAnalysis.CSharp; using UnityPipeline.Microsoft.CodeAnalysis.CSharp.Syntax; namespace Unity.Pipeline.Compilation { /// /// Rewrites C# string interpolation ($"...") into string.Format(fmt, args…) at the /// source level: the interpreter can't run the compiler's normal interpolation lowering /// (DefaultInterpolatedStringHandler, a ref struct), but it can call String.Format /// (bound in ). Applied by /// before an in-place reload body is compiled. /// /// Purely syntactic — no semantic model. Format specifiers, alignment, escaped braces, verbatim /// strings and nested interpolation are handled. The one unserved case is an interpolation whose /// target type is FormattableString/IFormattable: rewriting yields a string, /// so that rare usage becomes a compile error rather than a silent wrong result. /// sealed class InterpolationToFormatRewriter : CSharpSyntaxRewriter { public override SyntaxNode VisitInterpolatedStringExpression(InterpolatedStringExpressionSyntax node) { var fmt = new StringBuilder(); var args = new List(); int index = 0; foreach (var content in node.Contents) { switch (content) { case InterpolatedStringTextSyntax text: // ValueText decodes escapes (\t etc.) but leaves literal braces doubled // ({{ / }}) — exactly the escaping a Format string wants, so append as-is. fmt.Append(text.TextToken.ValueText); break; case InterpolationSyntax interp: fmt.Append('{').Append(index++); if (interp.AlignmentClause != null) fmt.Append(',').Append(interp.AlignmentClause.Value.ToString().Trim()); if (interp.FormatClause != null) fmt.Append(':').Append(interp.FormatClause.FormatStringToken.ValueText); fmt.Append('}'); // Visit the hole so nested interpolations rewrite bottom-up and later // member-qualification passes see it. args.Add(SyntaxFactory.Argument((ExpressionSyntax)Visit(interp.Expression))); break; } } // No holes: emit a plain string literal (no Format, no brace doubling), matching how // Roslyn treats a hole-less interpolated string. if (index == 0) { var raw = new StringBuilder(); foreach (var content in node.Contents) if (content is InterpolatedStringTextSyntax t) // A plain literal wants single braces, so undo the {{ / }} escaping ValueText keeps. raw.Append(t.TextToken.ValueText.Replace("{{", "{").Replace("}}", "}")); return SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(raw.ToString())) .WithTriviaFrom(node); } var allArgs = new List(args.Count + 1) { SyntaxFactory.Argument(SyntaxFactory.LiteralExpression( SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(fmt.ToString()))), }; allArgs.AddRange(args); // Passing holes as loose args lets Roslyn pick the Format overload: fixed arity for ≤3, // params object[] for ≥4 — both are bound on the interpreter side. return SyntaxFactory.InvocationExpression( SyntaxFactory.ParseExpression("string.Format"), SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(allArgs))) .WithTriviaFrom(node); } } }