Inspect BP pin connection at compile time

Verify node connectivity at BP compile-time, or run various other functors for each node of a Widget Blueprint function, macro, etc.

Inspect blueprint pin connection at compile time in Unreal Engine 4
Custom error messages (FTokenizedMessage) like this can be generated from your own custom set of requirements and evaluated at BP compile time.

Where the idea came from

I wanted a way to ensure some BlueprintImplementableEvent marked functions do return the expected blueprint generated variable, because it will inevitably be manipulated in native in a very extensive manner.

In my case, I had a UUserWidget-derived class with BlueprintImplementableEvent getter methods, returning UWidgetAnimation instances created from the Widget Blueprint editor.

I must point out that meta=(BindWidgetAnim) on e.g. UWidgetAnimation* MyAnim; makes the widget blueprint compiler mandate an animation named MyAnim, superseding the getter method approach described above.

Where to look

For UWidget derived classes

Override UWidget::ValidateCompiledDefaults, which is called at the end of Widget Blueprint compilation. Its purposefully giving derived classes the option to add extra blueprint compilation conditions on top.

Its definition is empty inside UWidget, hinting derived classes more about what stage of compilation is considered mutable to derived classes: the extra layer on top and nothing before, preferably.

/**
 * Called at the end of Widget Blueprint compilation.
 * Allows UMG elements to evaluate their default states and determine whether they are acceptable.
 * To trigger compilation failure, add an error to the log.
* Warnings and notes will be visible, but will not cause compiles to fail. */ virtual void ValidateCompiledDefaults(class IWidgetCompilerLog& CompileLog) const {}

With IWidgetCompilerLog, you can easily create rich compiler output messages with different severities: error, warning or note(/info). These are the main methods for that:

  • TSharedRef<FTokenizedMessage> IWidgetCompilerLog::Error(const FText& Message)
  • TSharedRef<FTokenizedMessage> IWidgetCompilerLog::Warning(const FText& Message)
  • TSharedRef<FTokenizedMessage> IWidgetCompilerLog::Note(const FText& Message)

Each method returns a TSharedRef<FTokenizedMessage> (FTokenizedMessage inherits from TSharedFromThis<FTokenizedMessage>), allowing further modification in the scope, such as adding even more tokens.

I think the term 'Token' is generic; It doesn't solely represent one type-related construct, similar to IO streams.

Expect more info about FTokenizedMessage in another, upcoming document.

Usage example

#define LOCTEXT_NAMESPACE "UMyUserWidget"

void UMyUserWidget::ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const
{
Super::ValidateCompiledDefaults(CompileLog); // Just habit

TSharedRef<FTokenizedMessage> Line =
CompileLog.Error(LOCTEXT("ValidateCompiledDefaults", "Simple message"));
}

#undef LOCTEXT_NAMESPACE

We'll be using the principle shown above to achieve what we want.

For AActor

This part will be written in the near future.


The code - Setting it up

To keep the groove flowing, we'll be housing the methods responsible for the job in namespaces. These namespaces may act as substitutes of your own library and its underlying structure, thus could very well be twisted or molded to your needs.

Modules added: "UnrealEd", "UMG"

Public/YourAliasEditorUtilities.h

#pragma once

#include "CoreMinimal.h"

class UEdGraphNode;
class UEdGraphPin;
class IWidgetCompilerLog;
class FBlueprintCompilerLog;

#if WITH_EDITOR

namespace FYourAliasEditorUtilities
{
    /**
     * Run a NodeQuery (functor) for each present blueprint graph node of a blueprint function, macro, etc.
     * We gain the ability (at BP compile-time) to inspect the status of actual blueprint graph nodes.
     *
     * @param Class         The class the FunctionNames belong in. Usually UObjectBase::GetClass()
     * @param GraphNames    The target graphs of Class. Could be a function, macro, etc.
     * @param NodeQuery     Functor to run for each present blueprint graph node
     */
    FORCENOINLINE void RunBPGraphNodeQuery(const UClass* Class, const TArray GraphNames,
        TFunction<void(const UBlueprint& Blueprint, const UFunction& Function, const UEdGraphNode& Node)> NodeQuery);

    /**
     * Run a functor for each not-connected pin of a blueprint (function) graph node.
     * The goal is to (at BP compile-time) see if a pin is not routed to anything, and respond to that.
     *
     * @param Class             The class the FunctionNames belong in. Usually UObjectBase::GetClass()
     * @param FunctionNames     The target function(s) of Class.
     * @param OnNotConnected    Functor to run for each present blueprint graph node
     */
    FORCENOINLINE void VerifyPinConnectivity(const UClass* Class, const TArray FunctionNames, const TArray PinNames,
        TFunction<void(const UBlueprint& Blueprint, const UFunction& Function, const UEdGraphPin& Pin)> OnNotConnected);    
}

namespace FYourAliasEditorMessageLog
{
    void ReturnValuePinConnectivity(IWidgetCompilerLog& CompileLog, const UClass* Class, const TArray FunctionNames);
}

#endif

Private/YourAliasEditorUtilities.cpp

#include "YourAliasEditorUtilities.h"
#include "YourAliasLogMacros.h" // Optional, https://ubyte.dev/docs/programmer/useful-logging-macros
#include "Engine/Blueprint.h"
#include "Editor/WidgetCompilerLog.h"
#include "UnrealEd/Public/EdGraphToken.h"

#define LOCTEXT_NAMESPACE "FYourAliasEditorUtilities"

void FYourAliasEditorUtilities::RunBPGraphNodeQuery(const UClass* Class, const TArray FunctionNames,
    TFunction<void(const UBlueprint& Blueprint, const UFunction& Function, const UEdGraphNode& Node)> NodeQuery)
{
    const UBlueprint* Blueprint = Cast<UBlueprint>(Class->ClassGeneratedBy);
    if (!Blueprint)
    {
//      YOURALIAS_LOG_ERROR("in Class '%s' is a native compiled-in class. There is no blueprint that caused the generation of this class.", *Class->GetName());
        return;
    }

    for (auto Itr = FunctionNames.CreateConstIterator(); Itr; ++Itr)
    {
        const FName& FunctionName = *Itr;
        const UFunction* Function = Class->FindFunctionByName(FunctionName);

        if (!Function)
        {
//          YOURALIAS_LOG_ERROR("'%s' (index %d inside provided FunctionNames parameter) doesn't exist in UClass '%s'", *FunctionName.ToString(), Itr.GetIndex(), *Class->GetName());
            continue;
        }

        TArray<UEdGraph*> AllGraphs;
        Blueprint->GetAllGraphs(AllGraphs);

        for (const UEdGraph* Graph : AllGraphs)
        {
            if (Graph && Graph->GetFName() == FunctionName)
            {
                for (int32 Index = 0; Index < Graph->Nodes.Num(); ++Index)
                {
                    if (UEdGraphNode* Node = Graph->Nodes[Index])
                    {
                        NodeQuery(*Blueprint, *Function, *Node);
                    }
                }
            }
        }
    }
}

void FYourAliasEditorUtilities::VerifyPinConnectivity(const UClass* Class, const TArray FunctionNames, const TArray PinNames,
    TFunction<void(const UBlueprint& Blueprint, const UFunction& Function, const UEdGraphPin& Pin)> OnNotConnected)
{
    RunBPGraphNodeQuery
    (
        /** Class */
        Class,

        /** FunctionNames */
        FunctionNames,

        /** NodeQuery */
        [&](const UBlueprint& Blueprint, const UFunction& Function, const UEdGraphNode& Node)
        {
            for (int32 PinIndex = 0; PinIndex < Node.Pins.Num(); ++PinIndex)
            {
                const UEdGraphPin* Pin = Node.Pins[PinIndex];

                if (Pin && PinNames.Contains(Pin->PinName) && !Pin->HasAnyConnections())
                {
                    OnNotConnected(Blueprint, Function, *Pin);
                }
            }
        }
    );
}

void FYourAliasEditorMessageLog::ReturnValuePinConnectivity(IWidgetCompilerLog& CompileLog, const UClass* Class, const TArray FunctionNames)
{
    FYourAliasEditorUtilities::VerifyPinConnectivity
    (
        /** Class */
        Class,

        /** FunctionNames */
        FunctionNames,

        /** PinNames */
        { FName("ReturnValue") },

        /** OnNotConnected */
        [&](const UBlueprint& Blueprint, const UFunction& Function, const UEdGraphPin& Pin)
        {
            TSharedRef Line = CompileLog.Error(FText::Format(
                LOCTEXT("ReturnValuePinConnectivity", "{0} needs a valid \"{1}\" pin connection. See "),
                FText::FromString(Function.GetName()),
                Pin.GetDisplayName()));

            TArray<UEdGraphNode*> SourceNodes;
            FEdGraphToken::Create(&Pin, Blueprint.CurrentMessageLog, *Line, SourceNodes);
        }
    );
}

#undef LOCTEXT_NAMESPACE

Replace every instance of 'YourAlias' with your project's alias. It's usually between 2 and 6 characters.

It consists of two parts:

  • FYourAliasEditorUtilities
    Generalizes editor related concepts through various methods. Excellent for hiding excessive details.
  • FYourAliasEditorMessageLog
    Builds further on FYourAliasEditorUtilities's methods as a more goal-achieving derivative: It specifically houses methods involved in printing messages to logs.

As you can see, by having functors as argument, we allow a comfortable range of variations to be build wherever we want. It could be in your next library method, or straight in a class that needs a custom approach.

The code - Example of using it

To give you an idea, here's how I used it:

void UPickupNotification::ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const
{
    Super::ValidateCompiledDefaults(CompileLog);
    
    FRageEditorMessageLog::ReturnValuePinConnectivity(
        CompileLog, GetClass(),
        { GET_FUNCTION_NAME_CHECKED(UPickupNotification, GetFadeInAnim),
          GET_FUNCTION_NAME_CHECKED(UPickupNotification, GetFadeOutAnim) }
    );
}