fix: Address CodeRabbit review issues and improve robustness

Critical Bug Fixes:
- Fix operator precedence bug in ManageAsset.cs that could cause null reference exceptions
- Fix GameObject memory leak in primitive creation when name validation fails
- Add proper cleanup with DestroyImmediate when primitive creation fails

ComponentResolver Integration:
- Replace fragile string-based GetComponent() calls with robust ComponentResolver
- Add ComponentResolver integration in ManageAsset.cs for component lookups
- Add fallback to string-based lookup in ManageGameObject.cs for compatibility

Enhanced Error Handling:
- Surface specific ComponentResolver error context in ScriptableObject creation failures
- Add support for setting private [SerializeField] fields in property matching
- Improve debugging with detailed error messages

Assembly Definition Fixes:
- Configure TestAsmdef as Editor-only to prevent build bloat
- Add explicit TestAsmdef reference to test assembly for proper component resolution
- Fix ComponentResolverTests to use accessible CustomComponent instead of TicTacToe3D

Code Quality:
- Disable nullable reference types for legacy codebase to eliminate 100+ warnings
- Maintain backward compatibility while improving reliability

All 45 unit tests pass, ensuring no regressions while significantly improving robustness.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
main
David Sarno 2025-09-03 08:27:34 -07:00
parent aac237c5cf
commit 17ad011b42
5 changed files with 59 additions and 18 deletions

View File

@ -2,12 +2,12 @@
"name": "TestAsmdef", "name": "TestAsmdef",
"rootNamespace": "TestNamespace", "rootNamespace": "TestNamespace",
"references": [], "references": [],
"includePlatforms": [], "includePlatforms": ["Editor"],
"excludePlatforms": [], "excludePlatforms": [],
"allowUnsafeCode": false, "allowUnsafeCode": false,
"overrideReferences": false, "overrideReferences": false,
"precompiledReferences": [], "precompiledReferences": [],
"autoReferenced": true, "autoReferenced": false,
"defineConstraints": [], "defineConstraints": [],
"versionDefines": [], "versionDefines": [],
"noEngineReferences": false "noEngineReferences": false

View File

@ -3,6 +3,7 @@
"rootNamespace": "", "rootNamespace": "",
"references": [ "references": [
"MCPForUnity.Editor", "MCPForUnity.Editor",
"TestAsmdef",
"UnityEngine.TestRunner", "UnityEngine.TestRunner",
"UnityEditor.TestRunner" "UnityEditor.TestRunner"
], ],

View File

@ -31,11 +31,11 @@ namespace MCPForUnityTests.Editor.Tools
[Test] [Test]
public void TryResolve_ReturnsTrue_ForCustomComponentShortName() public void TryResolve_ReturnsTrue_ForCustomComponentShortName()
{ {
bool result = ComponentResolver.TryResolve("TicTacToe3D", out Type type, out string error); bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
Assert.IsTrue(result, "Should resolve TicTacToe3D component"); Assert.IsTrue(result, "Should resolve CustomComponent");
Assert.IsNotNull(type, "Should return valid type"); Assert.IsNotNull(type, "Should return valid type");
Assert.AreEqual("TicTacToe3D", type.Name, "Should have correct type name"); Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type"); Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
Assert.IsEmpty(error, "Should have no error message"); Assert.IsEmpty(error, "Should have no error message");
} }

View File

@ -7,6 +7,7 @@ using Newtonsoft.Json.Linq;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using MCPForUnity.Editor.Helpers; // For Response class using MCPForUnity.Editor.Helpers; // For Response class
using static MCPForUnity.Editor.Tools.ManageGameObject; // For ComponentResolver
#if UNITY_6000_0_OR_NEWER #if UNITY_6000_0_OR_NEWER
using PhysicsMaterialType = UnityEngine.PhysicsMaterial; using PhysicsMaterialType = UnityEngine.PhysicsMaterial;
@ -207,9 +208,10 @@ namespace MCPForUnity.Editor.Tools
|| !typeof(ScriptableObject).IsAssignableFrom(scriptType) || !typeof(ScriptableObject).IsAssignableFrom(scriptType)
) )
{ {
return Response.Error( var reason = scriptType == null
$"Script class '{scriptClassName}' not found or does not inherit from ScriptableObject." ? (string.IsNullOrEmpty(error) ? "Type not found." : error)
); : "Type found but does not inherit from ScriptableObject.";
return Response.Error($"Script class '{scriptClassName}' invalid: {reason}");
} }
ScriptableObject so = ScriptableObject.CreateInstance(scriptType); ScriptableObject so = ScriptableObject.CreateInstance(scriptType);
@ -353,10 +355,18 @@ namespace MCPForUnity.Editor.Tools
&& componentProperties.HasValues && componentProperties.HasValues
) // e.g., {"bobSpeed": 2.0} ) // e.g., {"bobSpeed": 2.0}
{ {
// Find the component on the GameObject using the name from the JSON key // Resolve component type via ComponentResolver, then fetch by Type
// Using GetComponent(string) is convenient but might require exact type name or be ambiguous. Component targetComponent = null;
// Consider using ComponentResolver if needed for more complex scenarios. if (ComponentResolver.TryResolve(componentName, out var compType, out var compError))
Component targetComponent = gameObject.GetComponent(componentName); {
targetComponent = gameObject.GetComponent(compType);
}
else
{
Debug.LogWarning(
$"[ManageAsset.ModifyAsset] Failed to resolve component '{componentName}' on '{gameObject.name}': {compError}"
);
}
if (targetComponent != null) if (targetComponent != null)
{ {
@ -937,8 +947,8 @@ namespace MCPForUnity.Editor.Tools
{ {
string propName = floatProps["name"]?.ToString(); string propName = floatProps["name"]?.ToString();
if ( if (
!string.IsNullOrEmpty(propName) && floatProps["value"]?.Type == JTokenType.Float !string.IsNullOrEmpty(propName) &&
|| floatProps["value"]?.Type == JTokenType.Integer (floatProps["value"]?.Type == JTokenType.Float || floatProps["value"]?.Type == JTokenType.Integer)
) )
{ {
try try

View File

@ -1,4 +1,4 @@
#nullable enable #nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -289,11 +289,16 @@ namespace MCPForUnity.Editor.Tools
newGo = GameObject.CreatePrimitive(type); newGo = GameObject.CreatePrimitive(type);
// Set name *after* creation for primitives // Set name *after* creation for primitives
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{
newGo.name = name; newGo.name = name;
}
else else
{
UnityEngine.Object.DestroyImmediate(newGo); // cleanup leak
return Response.Error( return Response.Error(
"'name' parameter is required when creating a primitive." "'name' parameter is required when creating a primitive."
); // Name is essential );
}
createdNewObject = true; createdNewObject = true;
} }
catch (ArgumentException) catch (ArgumentException)
@ -1493,7 +1498,18 @@ namespace MCPForUnity.Editor.Tools
Component targetComponentInstance = null Component targetComponentInstance = null
) )
{ {
Component targetComponent = targetComponentInstance ?? targetGo.GetComponent(compName); Component targetComponent = targetComponentInstance;
if (targetComponent == null)
{
if (ComponentResolver.TryResolve(compName, out var compType, out var compError))
{
targetComponent = targetGo.GetComponent(compType);
}
else
{
targetComponent = targetGo.GetComponent(compName); // fallback to string-based lookup
}
}
if (targetComponent == null) if (targetComponent == null)
{ {
return Response.Error( return Response.Error(
@ -1627,6 +1643,20 @@ namespace MCPForUnity.Editor.Tools
Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); Debug.LogWarning($"[SetProperty] Conversion failed for field '{memberName}' (Type: {fieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}");
} }
} }
else
{
// Try NonPublic [SerializeField] fields
var npField = type.GetField(memberName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (npField != null && npField.GetCustomAttribute<SerializeField>() != null)
{
object convertedValue = ConvertJTokenToType(value, npField.FieldType, inputSerializer);
if (convertedValue != null || value.Type == JTokenType.Null)
{
npField.SetValue(target, convertedValue);
return true;
}
}
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -2199,7 +2229,7 @@ namespace MCPForUnity.Editor.Tools
t.Name.Equals(q, StringComparison.Ordinal) || t.Name.Equals(q, StringComparison.Ordinal) ||
(t.FullName?.Equals(q, StringComparison.Ordinal) ?? false); (t.FullName?.Equals(q, StringComparison.Ordinal) ?? false);
private static bool IsValidComponent(Type? t) => private static bool IsValidComponent(Type t) =>
t != null && typeof(Component).IsAssignableFrom(t); t != null && typeof(Component).IsAssignableFrom(t);
private static void Cache(Type t) private static void Cache(Type t)