267 lines
11 KiB
C#
267 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
using Unity.Pipeline.Compilation;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Pipeline.HotReload
|
|
{
|
|
/// <summary>
|
|
/// Transforms [HotReload] method bodies (edited in place on a MonoBehaviour) into static
|
|
/// hot reload override methods that the registry can dispatch to.
|
|
///
|
|
/// The transform is driven by a Roslyn <see cref="SemanticModel"/>: every identifier that binds
|
|
/// to an instance member of the declaring type (or a base, e.g. <c>transform</c> on Component) is
|
|
/// rewritten to <c>instance.<member></c>; locals, parameters, static members and Unity
|
|
/// built-ins are left untouched. The output mirrors the helper workflow's override shape:
|
|
///
|
|
/// [HotReloadOverrideMethod("Type.Method")]
|
|
/// public static <ret> Method(Type instance, <params>) { ...rewritten body... }
|
|
/// </summary>
|
|
public static class SourceCodeTransformer
|
|
{
|
|
/// <summary>
|
|
/// Transform the [HotReload] methods named in <paramref name="methodBodies"/> into a
|
|
/// static override class. <paramref name="originalTypeDefinition"/> (the full source of the
|
|
/// file) is required so the semantic model can resolve member bindings in context.
|
|
/// </summary>
|
|
/// <param name="emitLineDirectives">
|
|
/// When true, each rewritten body is bracketed by <c>#line</c> directives that map it back to
|
|
/// the original source file so an attached debugger binds breakpoints in the file the user
|
|
/// edited. Requires <paramref name="originalFilePath"/>. The body is emitted with its original
|
|
/// whitespace (no <c>NormalizeWhitespace</c>) so line positions survive the transform.
|
|
/// </param>
|
|
/// <param name="originalFilePath">Absolute path of the source file, recorded in the #line directives.</param>
|
|
public static string TransformMethodBodies(
|
|
Dictionary<string, string> methodBodies,
|
|
string originalTypeName,
|
|
Dictionary<string, MethodSignatureInfo> originalMethodSignatures,
|
|
string originalTypeDefinition = null,
|
|
bool emitLineDirectives = false,
|
|
string originalFilePath = null)
|
|
{
|
|
if (string.IsNullOrEmpty(originalTypeDefinition))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"In-place transformation requires the full original source to build a semantic model.");
|
|
}
|
|
|
|
try
|
|
{
|
|
var tree = CSharpSyntaxTree.ParseText(originalTypeDefinition);
|
|
var root = tree.GetRoot();
|
|
|
|
var compilation = CSharpCompilation.Create(
|
|
"HotReloadInPlaceTransform",
|
|
new[] { tree },
|
|
RoslynCompilationService.GetMetadataReferences(),
|
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
|
var model = compilation.GetSemanticModel(tree);
|
|
|
|
var classDecl = root.DescendantNodes()
|
|
.OfType<ClassDeclarationSyntax>()
|
|
.FirstOrDefault(c => c.Identifier.ValueText == originalTypeName);
|
|
if (classDecl == null)
|
|
{
|
|
throw new InvalidOperationException($"Could not find class '{originalTypeName}' in source.");
|
|
}
|
|
|
|
var classSymbol = model.GetDeclaredSymbol(classDecl);
|
|
var rewriter = new InstanceMemberQualifier(model, classSymbol);
|
|
|
|
var sb = new StringBuilder();
|
|
foreach (var u in root.DescendantNodes().OfType<UsingDirectiveSyntax>())
|
|
sb.AppendLine(u.ToString());
|
|
sb.AppendLine("using Unity.Pipeline.HotReload;");
|
|
|
|
var namespaceName = GetNamespace(classDecl);
|
|
if (!string.IsNullOrEmpty(namespaceName))
|
|
sb.AppendLine($"using {namespaceName};");
|
|
sb.AppendLine();
|
|
|
|
sb.AppendLine($"public static class {originalTypeName}HotReloadOverrides");
|
|
sb.AppendLine("{");
|
|
|
|
foreach (var method in classDecl.Members.OfType<MethodDeclarationSyntax>())
|
|
{
|
|
var methodName = method.Identifier.ValueText;
|
|
if (!methodBodies.ContainsKey(methodName) || method.Body == null)
|
|
continue;
|
|
|
|
var rewrittenBody = (BlockSyntax)rewriter.Visit(method.Body);
|
|
var returnType = method.ReturnType.ToString();
|
|
|
|
var parameters = new List<string> { $"{originalTypeName} instance" };
|
|
parameters.AddRange(method.ParameterList.Parameters.Select(
|
|
p => $"{p.Type} {p.Identifier.ValueText}"));
|
|
|
|
sb.AppendLine($" [HotReloadOverrideMethod(\"{originalTypeName}.{methodName}\")]");
|
|
sb.AppendLine($" public static {returnType} {methodName}({string.Join(", ", parameters)})");
|
|
|
|
if (emitLineDirectives && !string.IsNullOrEmpty(originalFilePath))
|
|
{
|
|
// Map the body back to the original file. The rewriter only makes intra-line
|
|
// edits, so a single #line at the body's opening brace maps every line of the
|
|
// block; emit the body with original trivia (not NormalizeWhitespace) to keep
|
|
// line counts intact. #line hidden masks the generated scaffolding that follows.
|
|
var bodyLine = method.Body.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
|
|
var escapedPath = originalFilePath.Replace("\\", "\\\\");
|
|
sb.AppendLine($"#line {bodyLine} \"{escapedPath}\"");
|
|
sb.AppendLine(rewrittenBody.ToString());
|
|
sb.AppendLine("#line hidden");
|
|
}
|
|
else
|
|
{
|
|
sb.AppendLine(" " + rewrittenBody.NormalizeWhitespace().ToString());
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
|
|
sb.AppendLine("}");
|
|
|
|
var result = sb.ToString();
|
|
Debug.Log($"HotReload: Transformed {methodBodies.Count} in-place method(s) for {originalTypeName}");
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"HotReload: Error transforming in-place methods for {originalTypeName}: {ex.Message}");
|
|
throw new InvalidOperationException(
|
|
$"Failed to transform in-place methods for {originalTypeName}: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
private static string GetNamespace(SyntaxNode node)
|
|
{
|
|
for (var current = node.Parent; current != null; current = current.Parent)
|
|
{
|
|
if (current is NamespaceDeclarationSyntax ns)
|
|
return ns.Name.ToString();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rewrites implicit instance-member access (this.x or bare x) to instance.x using the
|
|
/// semantic model. Locals, parameters, static members and built-ins are left untouched.
|
|
/// </summary>
|
|
private class InstanceMemberQualifier : CSharpSyntaxRewriter
|
|
{
|
|
private readonly SemanticModel m_Model;
|
|
private readonly INamedTypeSymbol m_Type;
|
|
|
|
public InstanceMemberQualifier(SemanticModel model, INamedTypeSymbol type)
|
|
{
|
|
m_Model = model;
|
|
m_Type = type;
|
|
}
|
|
|
|
public override SyntaxNode VisitThisExpression(ThisExpressionSyntax node)
|
|
{
|
|
return SyntaxFactory.IdentifierName("instance").WithTriviaFrom(node);
|
|
}
|
|
|
|
public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
|
|
{
|
|
if (ShouldSkip(node))
|
|
return base.VisitIdentifierName(node);
|
|
|
|
if (IsImplicitInstanceMember(node))
|
|
return Qualify(node);
|
|
|
|
return base.VisitIdentifierName(node);
|
|
}
|
|
|
|
public override SyntaxNode VisitGenericName(GenericNameSyntax node)
|
|
{
|
|
// e.g. GetComponent<Renderer>() — visit type arguments first.
|
|
var visited = (GenericNameSyntax)base.VisitGenericName(node);
|
|
|
|
if (ShouldSkip(node))
|
|
return visited;
|
|
|
|
if (IsImplicitInstanceMember(node))
|
|
return Qualify(visited);
|
|
|
|
return visited;
|
|
}
|
|
|
|
private static bool ShouldSkip(SimpleNameSyntax node)
|
|
{
|
|
// Right-hand side of a member access (foo.Bar -> Bar), or part of a qualified name.
|
|
if (node.Parent is MemberAccessExpressionSyntax ma && ma.Name == node)
|
|
return true;
|
|
if (node.Parent is QualifiedNameSyntax)
|
|
return true;
|
|
if (node.Parent is MemberBindingExpressionSyntax)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
private bool IsImplicitInstanceMember(SimpleNameSyntax node)
|
|
{
|
|
var symbol = m_Model.GetSymbolInfo(node).Symbol;
|
|
if (symbol == null || symbol.IsStatic)
|
|
return false;
|
|
|
|
switch (symbol.Kind)
|
|
{
|
|
case SymbolKind.Field:
|
|
case SymbolKind.Property:
|
|
case SymbolKind.Method:
|
|
case SymbolKind.Event:
|
|
return InTypeHierarchy(symbol.ContainingType);
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool InTypeHierarchy(INamedTypeSymbol containingType)
|
|
{
|
|
if (containingType == null)
|
|
return false;
|
|
for (var t = m_Type; t != null; t = t.BaseType)
|
|
{
|
|
if (SymbolEqualityComparer.Default.Equals(t, containingType))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static SyntaxNode Qualify(SimpleNameSyntax node)
|
|
{
|
|
return SyntaxFactory.MemberAccessExpression(
|
|
SyntaxKind.SimpleMemberAccessExpression,
|
|
SyntaxFactory.IdentifierName("instance"),
|
|
node.WithoutTrivia())
|
|
.WithTriviaFrom(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Information about a method's signature for transformation purposes.
|
|
/// </summary>
|
|
public class MethodSignatureInfo
|
|
{
|
|
public string ReturnType { get; set; }
|
|
public List<ParameterInfo> Parameters { get; set; } = new List<ParameterInfo>();
|
|
public bool ReturnsValue => !string.IsNullOrEmpty(ReturnType) && ReturnType != "void";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Information about a method parameter.
|
|
/// </summary>
|
|
public class ParameterInfo
|
|
{
|
|
public string Type { get; set; }
|
|
public string Name { get; set; }
|
|
public bool HasDefaultValue { get; set; }
|
|
public string DefaultValue { get; set; }
|
|
}
|
|
}
|