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
8 changes: 8 additions & 0 deletions Aspid.FastTools/Assets/DevTests/SerializeReferences.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6785315564667727353
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2201127709315222250}
- component: {fileID: 6873221168316818700}
m_Layer: 0
m_Name: RequiredViolationsDevTest
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2201127709315222250
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6785315564667727353}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6873221168316818700
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6785315564667727353}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9600738624fa14a36ab7f3cc1c063e27, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Aspid.FastTools.DevTests.SerializeReferences.RequiredViolationsDevTest
_requiredReference:
rid: -2
_requiredTypeName:
references:
version: 2
RefIds:
- rid: -2
type: {class: , ns: , asm: }

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using UnityEngine;
using Aspid.FastTools.Types;

// ReSharper disable once CheckNamespace
namespace Aspid.FastTools.DevTests.SerializeReferences
{
public interface IRequiredDevTestPayload { }

[Serializable]
public sealed class RequiredDevTestPayload : IRequiredDevTestPayload
{
[SerializeField] private int _value;
}

// Dev-only fixture for manually verifying the Project References "Required violations" group and the Asset
// References REQUIRED badge: both fields are left unset on Prefabs/RequiredViolationsDevTest.prefab on purpose.
public sealed class RequiredViolationsDevTest : MonoBehaviour
{
[SerializeReference, TypeSelector(Required = true)]
private IRequiredDevTestPayload _requiredReference;

[TypeSelector(typeof(Component), Required = true)]
[SerializeField] private string _requiredTypeName;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,14 +1,110 @@
using System.Linq;
using UnityEditor;
using UnityEngine;
using NUnit.Framework;
using System.Text.RegularExpressions;

namespace Aspid.FastTools.SerializeReferences.Editors.Tests
{
/// <summary>
/// Coverage for <see cref="SerializeReferenceGateScanner.IsPendingMigration"/> — the gate's pre-filter that keeps
/// properly declared renames from ever warning or failing a build / CI run.
/// properly declared renames from ever warning or failing a build / CI run — and for <see cref="SerializeReferenceGateScanner.Scan"/>
/// itself surfacing unset required fields (the data source for the Project References "Required violations" group).
/// </summary>
[TestFixture]
internal sealed class SerializeReferenceGateScannerTests
{
private const string ProbeAssetPath = "Assets/__AspidGateScannerRequiredProbe__.asset";

// Scan(RequiredOnly) is the exact call the Project References "Required violations" group makes; this proves
// it surfaces both an unset managed reference and an unset [TypeSelector(Required = true)] string field on a
// saved asset (RequiredTestObject, shared fixture — see SerializeReferenceTestFixtures.cs).
[Test]
public void Scan_RequiredOnly_SurfacesUnsetRequiredFieldsOnSavedAsset()
{
var probe = ScriptableObject.CreateInstance<RequiredTestObject>();
try
{
AssetDatabase.CreateAsset(probe, ProbeAssetPath);
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(probe, out _, out long fileId);

var violations = SerializeReferenceGateScanner.Scan(GateOptions.RequiredOnly);
var forProbe = violations.Where(v => v.AssetPath == ProbeAssetPath && v.FileId == fileId).ToList();

Assert.IsTrue(forProbe.All(v => v.Kind == GateViolationKind.RequiredUnset));
Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredRef)),
"Unset [SerializeReference, TypeSelector(Required = true)] field must be reported.");
Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredString)),
"Unset [TypeSelector(Required = true)] string field must be reported.");
}
finally
{
AssetDatabase.DeleteAsset(ProbeAssetPath);
}
}

// ScanAssetRequiredFields is the scoped, single-asset entry point the Inspect Asset graph calls on every
// Rescan; it must agree with Scan(RequiredOnly) for that one asset without sweeping the whole project.
[Test]
public void ScanAssetRequiredFields_SavedAsset_MatchesProjectScan()
{
var probe = ScriptableObject.CreateInstance<RequiredTestObject>();
try
{
AssetDatabase.CreateAsset(probe, ProbeAssetPath);
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(probe, out _, out long fileId);

var violations = SerializeReferenceGateScanner.ScanAssetRequiredFields(ProbeAssetPath);
var forProbe = violations.Where(v => v.FileId == fileId).ToList();

Assert.IsTrue(forProbe.All(v => v.Kind == GateViolationKind.RequiredUnset));
Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredRef)),
"Unset [SerializeReference, TypeSelector(Required = true)] field must be reported.");
Assert.IsTrue(forProbe.Any(v => v.FieldPath == nameof(RequiredTestObject.requiredString)),
"Unset [TypeSelector(Required = true)] string field must be reported.");
}
finally
{
AssetDatabase.DeleteAsset(ProbeAssetPath);
}
}

[Test]
public void ScanAssetRequiredFields_NonCandidatePath_ReturnsEmpty()
{
Assert.AreEqual(0, SerializeReferenceGateScanner.ScanAssetRequiredFields("Assets/Fake.txt").Count);
}

// The Inspect Asset graph (SerializeReferenceGraphView) badges an empty [SerializeReference] slot as REQUIRED
// by matching its graph field path — built independently by SerializeReferenceGraphScanner straight from
// YAML — against this scan's GateViolation.FieldPath (a live SerializedProperty.propertyPath), after the same
// "[i]" -> ".Array.data[i]" normalization the view applies. This proves the two paths actually agree for a
// real saved asset, not just that each individually finds the field.
[Test]
public void ScanAssetRequiredFields_UnsetManagedReference_MatchesGraphScannerEmptyRootPath()
{
var probe = ScriptableObject.CreateInstance<RequiredTestObject>();
try
{
AssetDatabase.CreateAsset(probe, ProbeAssetPath);
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(probe, out _, out long fileId);

var violations = SerializeReferenceGateScanner.ScanAssetRequiredFields(ProbeAssetPath);
var document = SerializeReferenceGraphScanner.Build(ProbeAssetPath).Single(doc => doc.FileId == fileId);
var emptyRoot = document.Roots.Single(root => root.IsEmpty);

var normalizedGraphPath = Regex.Replace(emptyRoot.Label, @"\[(\d+)\]", ".Array.data[$1]");

Assert.IsTrue(
violations.Any(v => v.FileId == fileId && v.FieldPath == normalizedGraphPath),
$"Normalized graph path '{normalizedGraphPath}' must match a ScanAssetRequiredFields violation's FieldPath.");
}
finally
{
AssetDatabase.DeleteAsset(ProbeAssetPath);
}
}

// A [MovedFrom]-claimed stale name is a pending migration, not a violation: Unity migrates the reference in
// memory at load, so the gate must accept it — a properly declared rename can never warn or fail a build /
// CI run. A scene path exercises the trust-the-claim branch (constraints are unrecoverable for scenes).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@
-unity-text-align: middle-center;
}


/* --- Inline type picker (mirrors the Project Audit picker exactly) ---
The selector view expanded inline, welded into the card being fixed (see TogglePicker): translucent dark fill,
neutral border, 8px radius and a fixed height (the ListView needs bounds to virtualize). padding:0 plus the header /
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,14 @@
align-items: center;
}

/* The Required-violations card has no Fix all button hosting its header row, so the static row carries the same
4px horizontal padding and 6px gap the button gives the broken-type cards — keeping every card's header and
entry paths on one shared left edge. */
.aspid-fasttools-repair-references__group-header-row--static {
padding: 4px 4px;
margin-bottom: 6px;
}

.aspid-fasttools-repair-references__group-header {
flex-shrink: 1;
}
Expand Down Expand Up @@ -664,6 +672,15 @@
-unity-text-align: middle-right;
}

/* A required-violation row's "Component.field" column — same dim, right-pinned treatment as __group-entry-rid, one
column over for a different row shape (Required violations card has no rid). */
.aspid-fasttools-repair-references__group-entry-field {
flex-shrink: 0;
margin-left: 10px;
color: var(--aspid-colors-text-dark);
-unity-text-align: middle-right;
}

/* A compatible MonoScript dragged over the field highlights the header as a valid drop target. */
.aspid-fasttools-serialize-reference--drop-target {
background-color: var(--aspid-colors-bg-light);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ internal readonly struct GateOptions
public static GateOptions MissingOnly =>
new(true, false);

public static GateOptions RequiredOnly =>
new(false, true);

private GateOptions(bool scanMissingTypes, bool scanRequiredFields)
{
ScanMissingTypes = scanMissingTypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ public static IReadOnlyList<GateViolation> Scan(GateOptions options, Action<floa
return violations;
}

/// <summary>
/// Scoped required-field scan for a single asset, without a full project sweep.
/// </summary>
/// <remarks>
/// Reuses <see cref="Scan"/>'s per-path dispatch for <see cref="GateOptions.ScanRequiredFields"/>. Used by the
/// Inspect Asset graph, which needs one asset's violations on every Rescan; skipped for a path that is not
/// itself a scan candidate, matching every other consumer's <see cref="SerializeReferenceHelpers.IsScanCandidate"/>.
/// </remarks>
public static IReadOnlyList<GateViolation> ScanAssetRequiredFields(string assetPath)
{
var violations = new List<GateViolation>();
if (string.IsNullOrEmpty(assetPath) || !SerializeReferenceHelpers.IsScanCandidate(assetPath)) return violations;

ScriptRequiredFieldsCache.Clear();

if (SerializeReferenceHelpers.IsScene(assetPath)) CollectSceneRequiredViolations(assetPath, violations);
else CollectRequiredViolations(assetPath, violations);

return violations;
}

// A stored name claimed by exactly one declared [MovedFrom] is a pending migration, not a violation —
// Unity migrates it in memory at load — provided the target still fits the field's declared type.
// Scenes cannot be object-loaded to recover constraints, so a scene entry claimed by a rename is trusted.
Expand Down
Loading
Loading