Extending

Extending RefSafe Pro

RefSafe Pro’s scanning pipeline is open at four points: validation rules, issue fixers, report exporters, and fix suggestion providers.

Every extension point is discovered by reflection. Drop your class into any assembly that references RefSafe.Pro.Editor and it’s picked up on the next domain reload. There is no registration call.

Assembly setup

{
  "name": "YourStudio.RefSafeExtensions",
  "includePlatforms": ["Editor"],
  "references": ["RefSafe.Pro.Editor", "RefSafe.Pro.Core"]
}

Working examples ship in Assets/RefSafe/Pro/Samples/CustomValidationRuleSample/Editor/.

Custom validation rules

Two contracts, depending on what you’re inspecting:

ContractScansInvoked by
IValidationRuleEvery GameObject in scenes and prefabsReferenceDetector
IAssetValidationRuleScriptableObjects and materialsScope controllers
using System.Collections.Generic;
using RefSafe.Pro;
using UnityEngine;

[ValidationRule(
    "Empty GameObject",
    "Finds GameObjects with only a Transform component.",
    IssueSeverity.Info,
    enabledByDefault: false)]
public sealed class EmptyGameObjectRule : IValidationRule
{
    public IEnumerable<ScanIssue> Validate(
        GameObject gameObject,
        string sceneName,
        string scenePath,
        string assetPath)
    {
        if (gameObject.GetComponents<Component>().Length > 1) yield break;

        var location = new ReferenceLocation(
            sceneName, scenePath, assetPath,
            gameObject.name,
            gameObject.name,
            componentName: string.Empty,
            fieldName: string.Empty,
            gameObjectInstanceId: gameObject.GetInstanceID());

        yield return new ScanIssue(
            IssueType.UnusedAsset,
            IssueSeverity.Info,
            $"Empty GameObject '{gameObject.name}' has no components besides Transform",
            location);
    }
}

[ValidationRule] takes a display name, a description shown as settings help text, a default severity users can override, and whether the rule is on out of the box.

Authoring notes. Validate runs for every GameObject on every scanned asset, so yield early and keep the hot path allocation-free. Use SerializedFieldWalker if you need to iterate serialized fields. The rule appears in the Settings panel as soon as the attribute exists, even before the body does anything — handy for staged rollout.

Custom fixers

IIssueFixer resolves issues in place. Users trigger fixers per-issue, across a selection, or through Fix All.

using RefSafe.Pro;
using UnityEditor;
using UnityEngine;

[IssueFixer(order: -1)]
public sealed class EmptyGameObjectFixer : IIssueFixer
{
    public string FixDescription => "Delete the empty GameObject";

    public bool CanFix(ScanIssue issue)
        => issue.Type == IssueType.UnusedAsset
        && issue.Message.StartsWith("Empty GameObject");

    public bool Fix(ScanIssue issue)
    {
        var go = EditorUtility.InstanceIDToObject(issue.Location.GameObjectInstanceId) as GameObject;
        if (go == null) return false;
        Undo.DestroyObjectImmediate(go);
        return true;
    }
}

Ordering. Built-in fixers run at order 0. A negative order supersedes a built-in fixer for issues your CanFix accepts; a positive order runs after built-ins as a fallback.

Authoring notes. Wrap destructive work in Undo.RecordObject / Undo.DestroyObjectImmediate so Ctrl+Z works. Keep CanFix pure and cheap — it runs per-issue on every results refresh. When several fixers claim one IssueType, discriminate on Message, Location.ComponentName, or Location.FieldName. Return false unless the problem is genuinely resolved; returning true prematurely hides an issue that still exists.

Constructor dependencies. If your fixer needs services, skip the attribute and register explicitly:

FixerRegistry.Register(new MyFixer(RefSafeApp.Services.Resolve<ILogger>()), order: -1);

Custom exporters

IReportExporter turns a scan result into a file.

[ReportExporter("slack", "Slack Digest", "md")]
public sealed class SlackMarkdownExporter : IReportExporter
{
    public void Export(IReadOnlyList<ScanIssue> issues, string filePath)
    {
        File.WriteAllText(filePath, $"*RefSafe*: {issues.Count} issues");
    }
}

Your exporter immediately appears in the editor Export menu, works as --format slack on the CLI, and resolves via ExporterRegistry.Find("slack"). Ids must be unique — a duplicate logs a warning and is dropped.

Fix suggestion providers

IFixSuggestionProvider supplies the candidates behind the Replace with… dropdown. Implement it to rank replacements using your project’s own naming or folder conventions, which generally beats generic matching.

Suggest returns FixSuggestion values ordered by descending score. The UI renders the top three, so cap the list rather than returning everything.

Service container

RefSafe Pro composes itself through a small dependency-injection container rooted at RefSafeApp, constructed on [InitializeOnLoad].

using RefSafe.Pro;

ILogger logger = RefSafeApp.Services.Resolve<ILogger>();
IEventBus bus  = RefSafeApp.Services.Resolve<IEventBus>();
IClock clock   = RefSafeApp.Services.Resolve<IClock>();

Registered application-scope singletons:

AbstractionDefault implementationPurpose
ILoggerUnityLoggerLog routing
IEventBusEventBusPub/sub for cross-component events
IClockEditorClockAbstracted time source, for testability
IIgnoreListStoreSettingsIgnoreListStorePersisted ignore entries
IScanResultCacheJsonScanResultCacheLast-scan cache used for diffing
IScanHistoryStoreJsonScanHistoryStoreScan history persistence
IIncrementalScanTrackerEventBusIncrementalScanTrackerChanged-file queue for ChangedOnly

Events

var bus = RefSafeApp.Services.Resolve<IEventBus>();

IDisposable token = bus.Subscribe<ScannableAssetsChangedEvent>(e =>
{
    Debug.Log("Scan-relevant assets changed.");
});

token.Dispose();   // when you're done

Built-in payloads live in RefSafe.Pro.Platform.Events: AssetsChangedEvent carries any imported, deleted, or moved asset paths, and ScannableAssetsChangedEvent is a marker emitted when a .unity, .prefab, or .asset file changed. Your own events can be any readonly struct — the bus is generic over T.

Fault isolation

Custom rules, fixers, and exporters run inside a guarded invocation path. If yours throws, RefSafe catches it, finishes the scan, and reports a RuleFault issue rather than aborting. The stack trace goes to the Unity console.

Convenient during development, but don’t rely on it — a rule that throws on every GameObject produces a great deal of noise.

Stability

Public types live in the RefSafe.Pro namespace across both assemblies. Breaking changes to SDK contracts are reserved for major version bumps; new optional members may be added within a major version, so don’t assume closed sets. Prefer coding against the interfaces rather than concrete types — helpers like SerializedFieldWalker and the container implementation are public, but they’re internal building blocks.

The complete generated surface is in the API reference.