Skip to content
Open
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
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Added

- ILPP diagnostic errors should now be clickable in the editor to go to the source of the issue. (#4250)

### Changed

### Deprecated
Expand Down
185 changes: 181 additions & 4 deletions com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -560,14 +560,191 @@ public static TypeReference GetEnumAsInt(this TypeReference typeReference)
}
}

/// <summary>
/// Finds the starting index of a method signature declaration
/// </summary>
/// <param name="line">The line of text to scan</param>
/// <param name="name">The name of the method to search for</param>
/// <returns>The starting index of the signature declaration if a declaration was found; otherwise -1</returns>
private static int IndexOfSignature(string line, string name)
{
var column = line.IndexOf(name, StringComparison.Ordinal);
while (column >= 0)
{
// Skip any whitespace between the name and what follows it
var end = column + name.Length;
while (end < line.Length && char.IsWhiteSpace(line[end]))
{
end++;
}
if (end < line.Length && (line[end] == '(' || line[end] == '<'))
{
return column;
}

// Check if there's a next match
column = line.IndexOf(name, column + 1, StringComparison.Ordinal);
}
return -1;
}

/// <summary>
/// Finds the line of <paramref name="method"/>'s signature declaration.
/// </summary>
/// <returns>The sequence point, or null if the method has no debug information.</returns>
public static SequencePoint GetSignatureLocation(this MethodDefinition method)
{
// The SourceLocation is the first line of the method body.
var bodyPoint = method.GetSourceLocation();
if (bodyPoint == null)
{
return method.DeclaringType.GetSourceLocation();
}

try
{
var lines = File.ReadAllLines(bodyPoint.Document.Url);
var startIndex = Math.Min(bodyPoint.StartLine, lines.Length) - 1;
// Work backwards from the first line in the method body
for (var i = startIndex; i >= 0; i--)
{
// If this line includes the method definition: FooBar(
var column = IndexOfSignature(lines[i], method.Name);
if (column < 0)
{
// A line above the body that ends a statement or block belongs to the previous member.
if (i < startIndex && (lines[i].Contains(';') || lines[i].Contains('}')))
{
break;
}
continue;
}

return new SequencePoint(method.Body.Instructions[0], bodyPoint.Document)
{
StartLine = i + 1,
EndLine = i + 1,
StartColumn = column + 1,
EndColumn = column + 1 + method.Name.Length,
};
}
}
catch (Exception)
{
// We don't need to do anything on failure
}
// If we didn't find anything else, just use the first line of the method body that we found earlier
return bodyPoint;
}

/// <summary>
/// Finds the first visible line of <paramref name="method"/> in the user's source, so a diagnostic or log can point at it.
/// </summary>
/// <returns>The sequence point, or null if the method has no debug information.</returns>
public static SequencePoint GetSourceLocation(this MethodDefinition method)
{
foreach (var sequencePoint in method.DebugInformation.SequencePoints)
{
if (sequencePoint.IsHidden)
{
continue;
}
return sequencePoint;
}
return null;
}

/// <summary>
/// Finds the first visible line of any method in <paramref name="type"/>, so a diagnostic or log can at least open the right file.
/// </summary>
/// <returns>The sequence point, or null if the type has no debug information.</returns>
public static SequencePoint GetSourceLocation(this TypeDefinition type)
{
foreach (var method in type.Methods)
{
if (!method.HasBody)
{
continue;
}

var sequencePoint = method.GetSourceLocation();
if (sequencePoint != null)
{
return sequencePoint;
}
}

return null;
}

/// <summary>
/// Finds where <paramref name="field"/> is declared in the user's source. Fields carry no debug information of their own,
/// but a field initializer is compiled into the constructor with a sequence point on the declaration, so this looks for
/// the store to the field there. Falls back to <see cref="GetSourceLocation(TypeDefinition)"/> when the field has no initializer.
/// </summary>
/// <returns>The sequence point, or null if the declaring type has no debug information.</returns>
public static SequencePoint GetSourceLocation(this FieldDefinition field)
{
var storeOpCode = field.IsStatic ? OpCodes.Stsfld : OpCodes.Stfld;
foreach (var method in field.DeclaringType.Methods)
{
if (!method.IsConstructor || method.IsStatic != field.IsStatic || !method.HasBody)
{
continue;
}
SequencePoint lastVisible = null;
foreach (var instruction in method.Body.Instructions)
{
// Store the last visible line as the line we're looking for might not be visible
// Once we find the line we're looking for we want to return the last visible line
var sequencePoint = method.DebugInformation.GetSequencePoint(instruction);
if (sequencePoint != null && !sequencePoint.IsHidden)
{
lastVisible = sequencePoint;
}

// We're only looking for the store op codes on fields
var fieldReference = instruction.Operand as FieldReference;
if (instruction.OpCode != storeOpCode || fieldReference == null)
{
continue;
}

// If this is the field we're looking for and we have a visible instruction, then we've found what we're looking for!
if (fieldReference.Resolve() == field && lastVisible != null)
{
return lastVisible;
}
}
}

return field.DeclaringType.GetSourceLocation();
}

/// <summary>
/// Finds where <paramref name="member"/> is in the user's source. Called only when a diagnostic or log needs a location,
/// because finding a field's line scans its constructor's IL.
/// </summary>
/// <returns>The sequence point, or null if there is no debug information.</returns>
public static SequencePoint GetSourceLocation(this IMemberDefinition member)
{
return member switch
{
MethodDefinition method => method.GetSourceLocation(),
FieldDefinition field => field.GetSourceLocation(),
TypeDefinition type => type.GetSourceLocation(),
_ => null,
};
}

public static void AddError(this List<DiagnosticMessage> diagnostics, string message)
{
diagnostics.AddError((SequencePoint)null, message);
}

public static void AddError(this List<DiagnosticMessage> diagnostics, MethodDefinition methodDefinition, string message)
public static void AddError(this List<DiagnosticMessage> diagnostics, IMemberDefinition member, string message)
{
diagnostics.AddError(methodDefinition.DebugInformation.SequencePoints.FirstOrDefault(), message);
diagnostics.AddError(member?.GetSourceLocation(), message);
}

public static void AddError(this List<DiagnosticMessage> diagnostics, SequencePoint sequencePoint, string message)
Expand All @@ -587,9 +764,9 @@ public static void AddWarning(this List<DiagnosticMessage> diagnostics, string m
diagnostics.AddWarning((SequencePoint)null, message);
}

public static void AddWarning(this List<DiagnosticMessage> diagnostics, MethodDefinition methodDefinition, string message)
public static void AddWarning(this List<DiagnosticMessage> diagnostics, IMemberDefinition member, string message)
{
diagnostics.AddWarning(methodDefinition.DebugInformation.SequencePoints.FirstOrDefault(), message);
diagnostics.AddWarning(member?.GetSourceLocation(), message);
}

public static void AddWarning(this List<DiagnosticMessage> diagnostics, SequencePoint sequencePoint, string message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
if (type.HasInterface(CodeGenHelpers.INetworkSerializable_FullName))
{
m_Diagnostics.AddError($"{nameof(INetworkSerializeByMemcpy)} types may not implement {nameof(INetworkSerializable)} - choose one or the other.");
m_Diagnostics.AddError(type, $"{nameof(INetworkSerializeByMemcpy)} types may not implement {nameof(INetworkSerializable)} - choose one or the other.");
}
if (!type.IsValueType)
{
m_Diagnostics.AddError($"{nameof(INetworkSerializeByMemcpy)} types must be unmanaged types.");
m_Diagnostics.AddError(type, $"{nameof(INetworkSerializeByMemcpy)} types must be unmanaged types.");
}
}
}
Expand Down
Loading
Loading