RealtimeStyleTransferRuntime/Source/LyraEditor/Validation/EditorValidator.cpp

517 lines
20 KiB
C++
Raw Normal View History

2022-05-23 18:41:30 +00:00
// Copyright Epic Games, Inc. All Rights Reserved.
#include "EditorValidator.h"
2022-09-13 07:18:28 +00:00
#include "AssetRegistry/ARFilter.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Blueprint/BlueprintSupport.h"
#include "Containers/Map.h"
#include "Containers/Set.h"
#include "DataValidationModule.h"
2022-05-23 18:41:30 +00:00
#include "Editor.h"
2022-09-13 07:18:28 +00:00
#include "Editor/EditorEngine.h"
#include "EditorValidatorSubsystem.h"
#include "Engine/BlueprintCore.h"
#include "HAL/IConsoleManager.h"
#include "HAL/PlatformMisc.h"
2022-05-23 18:41:30 +00:00
#include "ISourceControlModule.h"
2022-09-13 07:18:28 +00:00
#include "ISourceControlOperation.h"
2022-05-23 18:41:30 +00:00
#include "ISourceControlProvider.h"
2022-09-13 07:18:28 +00:00
#include "ISourceControlState.h"
#include "Internationalization/Text.h"
#include "Logging/LogCategory.h"
#include "Logging/LogMacros.h"
#include "Logging/MessageLog.h"
#include "LyraEditor.h"
#include "Misc/AssertionMacros.h"
#include "Misc/AssetRegistryInterface.h"
2022-05-23 18:41:30 +00:00
#include "Misc/ConfigCacheIni.h"
2022-09-13 07:18:28 +00:00
#include "Misc/MessageDialog.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "Misc/ScopedSlowTask.h"
#include "Modules/ModuleManager.h"
2022-05-23 18:41:30 +00:00
#include "Settings/ProjectPackagingSettings.h"
2022-09-13 07:18:28 +00:00
#include "ShaderCompiler.h"
2022-05-23 18:41:30 +00:00
#include "SourceCodeNavigation.h"
2022-09-13 07:18:28 +00:00
#include "SourceControlOperations.h"
2022-05-23 18:41:30 +00:00
#include "Stats/StatsMisc.h"
2022-09-13 07:18:28 +00:00
#include "StudioAnalytics.h"
#include "Templates/SharedPointer.h"
#include "Trace/Detail/Channel.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "UObject/Package.h"
#include "UObject/SoftObjectPath.h"
#include "UObject/TopLevelAssetPath.h"
#include "UObject/UObjectIterator.h"
#include "UObject/WeakObjectPtr.h"
#include "UObject/WeakObjectPtrTemplates.h"
2022-05-23 18:41:30 +00:00
#define LOCTEXT_NAMESPACE "EditorValidator"
int32 GMaxAssetsChangedByAHeader = 200;
static FAutoConsoleVariableRef CVarMaxAssetsChangedByAHeader(TEXT("EditorValidator.MaxAssetsChangedByAHeader"), GMaxAssetsChangedByAHeader, TEXT("The maximum number of assets to check for content validation based on a single header change."), ECVF_Default);
bool UEditorValidator::bAllowFullValidationInEditor = false;
TArray<FString> FLyraValidationMessageGatherer::IgnorePatterns;
UEditorValidator::UEditorValidator()
: Super()
{
}
void UEditorValidator::ValidateCheckedOutContent(bool bInteractive, const EDataValidationUsecase InValidationUsecase)
{
FStudioAnalytics::RecordEvent(TEXT("ValidateContent"));
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
if (AssetRegistryModule.Get().IsLoadingAssets())
{
if (bInteractive)
{
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("DiscoveringAssets", "Still discovering assets. Try again once it is complete."));
}
else
{
UE_LOG(LogLyraEditor, Display, TEXT("Could not run ValidateCheckedOutContent because asset discovery was still being done."));
}
return;
}
TArray<FString> ChangedPackageNames;
TArray<FString> DeletedPackageNames;
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
if (ISourceControlModule::Get().IsEnabled())
{
// Request the opened files at filter construction time to make sure checked out files have the correct state for the filter
TSharedRef<FUpdateStatus, ESPMode::ThreadSafe> UpdateStatusOperation = ISourceControlOperation::Create<FUpdateStatus>();
UpdateStatusOperation->SetGetOpenedOnly(true);
SourceControlProvider.Execute(UpdateStatusOperation, EConcurrency::Synchronous);
TArray<FSourceControlStateRef> CheckedOutFiles = SourceControlProvider.GetCachedStateByPredicate(
[](const FSourceControlStateRef& State) { return State->IsCheckedOut() || State->IsAdded() || State->IsDeleted(); }
);
for (const FSourceControlStateRef& FileState : CheckedOutFiles)
{
FString Filename = FileState->GetFilename();
if (FPackageName::IsPackageFilename(Filename))
{
// Assets
FString PackageName;
if (FPackageName::TryConvertFilenameToLongPackageName(Filename, PackageName))
{
if (FileState->IsDeleted())
{
DeletedPackageNames.Add(PackageName);
}
else
{
ChangedPackageNames.Add(PackageName);
}
}
}
else if (Filename.EndsWith(TEXT(".h")))
{
// Source code header changes for classes may cause issues in assets based on those classes
UEditorValidator::GetChangedAssetsForCode(AssetRegistryModule.Get(), Filename, ChangedPackageNames);
}
}
}
bool bAnyIssuesFound = false;
TArray<FString> AllWarningsAndErrors;
{
if (bInteractive)
{
bAllowFullValidationInEditor = true;
// We will be flushing shader compile as we load materials, so dont let other shader warnings be attributed incorrectly to the package that is loading.
if (GShaderCompilingManager)
{
FScopedSlowTask SlowTask(0.f, LOCTEXT("CompilingShadersBeforeCheckingContentTask", "Finishing shader compiles before checking content..."));
SlowTask.MakeDialog();
GShaderCompilingManager->FinishAllCompilation();
}
}
{
FScopedSlowTask SlowTask(0.f, LOCTEXT("CheckingContentTask", "Checking content..."));
SlowTask.MakeDialog();
if (!ValidatePackages(ChangedPackageNames, DeletedPackageNames, 2000, AllWarningsAndErrors, InValidationUsecase))
{
bAnyIssuesFound = true;
}
}
if (bInteractive)
{
bAllowFullValidationInEditor = false;
}
}
{
FLyraValidationMessageGatherer ScopedMessageGatherer;
if (!ValidateProjectSettings())
{
bAnyIssuesFound = true;
}
AllWarningsAndErrors.Append(ScopedMessageGatherer.GetAllWarningsAndErrors());
}
if (bInteractive)
{
const bool bAtLeastOneMessage = (AllWarningsAndErrors.Num() != 0);
if (bAtLeastOneMessage)
{
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("ContentValidationFailed", "!!!!!!! Your checked out content has issues. Don't submit until they are fixed !!!!!!!\r\n\r\nSee the MessageLog and OutputLog for details"));
}
else if (bAnyIssuesFound)
{
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("ContentValidationFailedWithNoMessages", "No errors or warnings were found, but there was an error return code. Look in the OutputLog and log file for details. You may need engineering help."));
}
else
{
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("ContentValidationPassed", "All checked out content passed. Nice job."));
}
}
}
bool UEditorValidator::ValidatePackages(const TArray<FString>& ExistingPackageNames, const TArray<FString>& DeletedPackageNames, int32 MaxPackagesToLoad, TArray<FString>& OutAllWarningsAndErrors, const EDataValidationUsecase InValidationUsecase)
{
bool bAnyIssuesFound = false;
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
TArray<FString> AllPackagesToValidate = ExistingPackageNames;
for (const FString& DeletedPackageName : DeletedPackageNames)
{
UE_LOG(LogLyraEditor, Display, TEXT("Adding referencers for deleted package %s to be verified"), *DeletedPackageName);
TArray<FName> PackageReferencers;
AssetRegistry.GetReferencers(FName(*DeletedPackageName), PackageReferencers, UE::AssetRegistry::EDependencyCategory::Package);
for (const FName& Referencer : PackageReferencers)
{
const FString ReferencerString = Referencer.ToString();
if (!DeletedPackageNames.Contains(ReferencerString) && !IsInUncookedFolder(ReferencerString))
{
UE_LOG(LogLyraEditor, Display, TEXT(" Deleted package referencer %s was added to the queue to be verified"), *ReferencerString);
AllPackagesToValidate.Add(ReferencerString);
}
}
}
FMessageLog DataValidationLog("AssetCheck");
DataValidationLog.NewPage(LOCTEXT("ValidatePackages", "Validate Packages"));
if (AllPackagesToValidate.Num() > MaxPackagesToLoad)
{
// Too much changed to verify, just pass it.
FString WarningMessage = FString::Printf(TEXT("Assets to validate (%d) exceeded -MaxPackagesToLoad=(%d). Skipping existing package validation."), AllPackagesToValidate.Num(), MaxPackagesToLoad);
UE_LOG(LogLyraEditor, Warning, TEXT("%s"), *WarningMessage);
OutAllWarningsAndErrors.Add(WarningMessage);
DataValidationLog.Warning(FText::FromString(WarningMessage));
}
else
{
// Load all packages that match the file filter string
TArray<FAssetData> AssetsToCheck;
for (const FString& PackageName : AllPackagesToValidate)
{
if (FPackageName::IsValidLongPackageName(PackageName) && !IsInUncookedFolder(PackageName))
{
int32 OldNumAssets = AssetsToCheck.Num();
AssetRegistry.GetAssetsByPackageName(FName(*PackageName), AssetsToCheck, true);
if (AssetsToCheck.Num() == OldNumAssets)
{
FString WarningMessage;
// See if the file exists at all. Otherwise, the package contains no assets.
if (FPackageName::DoesPackageExist(PackageName))
{
WarningMessage = FString::Printf(TEXT("Found no assets in package '%s'"), *PackageName);
}
else
{
if (ISourceControlModule::Get().IsEnabled())
{
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
FString PackageFilename = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
TSharedPtr<ISourceControlState, ESPMode::ThreadSafe> FileState = SourceControlProvider.GetState(PackageFilename, EStateCacheUsage::ForceUpdate);
if (FileState->IsAdded())
{
WarningMessage = FString::Printf(TEXT("Package '%s' is missing from disk. It is marked for add in perforce but missing from your hard drive."), *PackageName);
}
if (FileState->IsCheckedOut())
{
WarningMessage = FString::Printf(TEXT("Package '%s' is missing from disk. It is checked out in perforce but missing from your hard drive."), *PackageName);
}
}
if (WarningMessage.IsEmpty())
{
WarningMessage = FString::Printf(TEXT("Package '%s' is missing from disk."), *PackageName);
}
}
ensure(!WarningMessage.IsEmpty());
UE_LOG(LogLyraEditor, Warning, TEXT("%s"), *WarningMessage);
OutAllWarningsAndErrors.Add(WarningMessage);
DataValidationLog.Warning(FText::FromString(WarningMessage));
bAnyIssuesFound = true;
}
}
}
if (AssetsToCheck.Num() > 0)
{
// Preload all assets to check, so load warnings can be handled separately from validation warnings
{
for (const FAssetData& AssetToCheck : AssetsToCheck)
{
if (!AssetToCheck.IsAssetLoaded())
{
2022-09-13 07:18:28 +00:00
UE_LOG(LogLyraEditor, Display, TEXT("Preloading %s..."), *AssetToCheck.GetObjectPathString());
2022-05-23 18:41:30 +00:00
// Start listening for load warnings
FLyraValidationMessageGatherer ScopedPreloadMessageGatherer;
// Load the asset
AssetToCheck.GetAsset();
if (ScopedPreloadMessageGatherer.GetAllWarningsAndErrors().Num() > 0)
{
// Repeat all errant load warnings as errors, so other CIS systems can treat them more severely (i.e. Build health will create an issue and assign it to a developer)
for (const FString& LoadWarning : ScopedPreloadMessageGatherer.GetAllWarnings())
{
UE_LOG(LogLyraEditor, Error, TEXT("%s"), *LoadWarning);
}
OutAllWarningsAndErrors.Append(ScopedPreloadMessageGatherer.GetAllWarningsAndErrors());
bAnyIssuesFound = true;
}
}
}
}
// Run all validators now.
FLyraValidationMessageGatherer ScopedMessageGatherer;
FValidateAssetsSettings Settings;
FValidateAssetsResults Results;
Settings.bSkipExcludedDirectories = true;
Settings.bShowIfNoFailures = true;
Settings.ValidationUsecase = InValidationUsecase;
const bool bHasInvalidFiles = GEditor->GetEditorSubsystem<UEditorValidatorSubsystem>()->ValidateAssetsWithSettings(AssetsToCheck, Settings, Results) > 0;
if (bHasInvalidFiles || ScopedMessageGatherer.GetAllWarningsAndErrors().Num() > 0)
{
OutAllWarningsAndErrors.Append(ScopedMessageGatherer.GetAllWarningsAndErrors());
bAnyIssuesFound = true;
}
}
}
return !bAnyIssuesFound;
}
bool UEditorValidator::ValidateProjectSettings()
{
bool bSuccess = true;
FMessageLog ValidationLog("AssetCheck");
{
bool bDeveloperMode = false;
GConfig->GetBool(TEXT("/Script/PythonScriptPlugin.PythonScriptPluginSettings"), TEXT("bDeveloperMode"), /*out*/ bDeveloperMode, GEngineIni);
if (bDeveloperMode)
{
const FString ErrorMessage(TEXT("The project setting version of Python's bDeveloperMode should not be checked in. Use the editor preference version instead!"));
UE_LOG(LogLyraEditor, Error, TEXT("%s"), *ErrorMessage);
ValidationLog.Error(FText::AsCultureInvariant(ErrorMessage));
bSuccess = false;
}
}
return bSuccess;
}
bool UEditorValidator::IsInUncookedFolder(const FString& PackageName, FString* OutUncookedFolderName)
{
const UProjectPackagingSettings* const PackagingSettings = GetDefault<UProjectPackagingSettings>();
check(PackagingSettings);
for (const FDirectoryPath& DirectoryToNeverCook : PackagingSettings->DirectoriesToNeverCook)
{
const FString& UncookedFolder = DirectoryToNeverCook.Path;
if (PackageName.StartsWith(UncookedFolder))
{
if (OutUncookedFolderName)
{
FString FolderToReport = UncookedFolder.StartsWith(TEXT("/Game/")) ? UncookedFolder.RightChop(6) : UncookedFolder;
if (FolderToReport.EndsWith(TEXT("/")))
{
*OutUncookedFolderName = FolderToReport.LeftChop(1);
}
else
{
*OutUncookedFolderName = FolderToReport;
}
}
return true;
}
}
return false;
}
bool UEditorValidator::ShouldAllowFullValidation()
{
return IsRunningCommandlet() || bAllowFullValidationInEditor;
}
bool UEditorValidator::CanValidateAsset_Implementation(UObject* InAsset) const
{
if (InAsset)
{
FString PackageName = InAsset->GetOutermost()->GetName();
if (!IsInUncookedFolder(PackageName))
{
return true;
}
}
return false;
}
void UEditorValidator::GetChangedAssetsForCode(IAssetRegistry& AssetRegistry, const FString& ChangedHeaderLocalFilename, TArray<FString>& OutChangedPackageNames)
{
static struct FCachedNativeClasses
{
public:
FCachedNativeClasses()
{
static const FName ModuleNameFName = "ModuleName";
static const FName ModuleRelativePathFName = "ModuleRelativePath";
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass* TestClass = *ClassIt;
if (TestClass->HasAnyClassFlags(CLASS_Native))
{
FAssetData ClassAssetData(TestClass);
FString ModuleName, ModuleRelativePath;
ClassAssetData.GetTagValue(ModuleNameFName, ModuleName);
ClassAssetData.GetTagValue(ModuleRelativePathFName, ModuleRelativePath);
Classes.Add(ModuleName + TEXT("+") + ModuleRelativePath, TestClass);
}
}
}
TArray<TWeakObjectPtr<UClass>> GetClassesInHeader(const FString& ModuleName, const FString& ModuleRelativePath)
{
TArray<TWeakObjectPtr<UClass>> ClassesInHeader;
Classes.MultiFind(ModuleName + TEXT("+") + ModuleRelativePath, ClassesInHeader);
return ClassesInHeader;
}
private:
TMultiMap<FString, TWeakObjectPtr<UClass>> Classes;
} NativeClassCache;
const TArray<FString>& ModuleNames = FSourceCodeNavigation::GetSourceFileDatabase().GetModuleNames();
const FString* Module = ModuleNames.FindByPredicate([ChangedHeaderLocalFilename](const FString& ModuleBuildPath) {
const FString ModuleFullPath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(ModuleBuildPath));
if (ChangedHeaderLocalFilename.StartsWith(ModuleFullPath))
{
return true;
}
return false;
});
if (Module)
{
SCOPE_LOG_TIME_IN_SECONDS(TEXT("Looking for blueprints affected by code changes"), nullptr);
const FString FoundModulePath = FPaths::ConvertRelativePathToFull(FPaths::GetPath(*Module));
const FString FoundModulePathWithSlash = FoundModulePath / TEXT("");
FString ChangedHeaderReleativeToModule = ChangedHeaderLocalFilename;
FPaths::MakePathRelativeTo(ChangedHeaderReleativeToModule, *FoundModulePathWithSlash);
FString ChangedHeaderModule = FPaths::GetBaseFilename(FoundModulePath);
// STEP 1 - Find all the native classes inside the header that changed.
TArray<TWeakObjectPtr<UClass>> ClassList = NativeClassCache.GetClassesInHeader(ChangedHeaderModule, ChangedHeaderReleativeToModule);
// STEP 2 - We now need to convert the set of native classes into actual derived blueprints.
bool bTooManyFiles = false;
TArray<FAssetData> BlueprintsDerivedFromNativeModifiedClasses;
for (TWeakObjectPtr<UClass> ModifiedClassPtr : ClassList)
{
// If we capped out on maximum number of modified files for a single header change, don't try to keep looking for more stuff.
if (bTooManyFiles)
{
break;
}
if (UClass* ModifiedClass = ModifiedClassPtr.Get())
{
// This finds all native derived blueprints, both direct subclasses, or subclasses of subclasses.
2022-09-13 07:18:28 +00:00
TSet<FTopLevelAssetPath> DerivedClassNames;
TArray<FTopLevelAssetPath> ClassNames;
ClassNames.Add(ModifiedClass->GetClassPathName());
AssetRegistry.GetDerivedClassNames(ClassNames, TSet<FTopLevelAssetPath>(), DerivedClassNames);
2022-05-23 18:41:30 +00:00
UE_LOG(LogLyraEditor, Display, TEXT("Validating Subclasses of %s in %s + %s"), *ModifiedClass->GetName(), *ChangedHeaderModule, *ChangedHeaderReleativeToModule);
FARFilter Filter;
Filter.bRecursiveClasses = true;
2022-09-13 07:18:28 +00:00
Filter.ClassPaths.Add(UBlueprintCore::StaticClass()->GetClassPathName());
2022-05-23 18:41:30 +00:00
// We enumerate all assets to find any blueprints who inherit from native classes directly - or
// from other blueprints.
AssetRegistry.EnumerateAssets(Filter, [&BlueprintsDerivedFromNativeModifiedClasses, &bTooManyFiles, &DerivedClassNames, ChangedHeaderModule, ChangedHeaderReleativeToModule](const FAssetData& AssetData)
{
FString PackageName = AssetData.PackageName.ToString();
// Don't check data-only blueprints, we'll be here all day.
if (!AssetData.GetTagValueRef<bool>(FBlueprintTags::IsDataOnly) && !UEditorValidator::IsInUncookedFolder(PackageName))
{
// Need to get the generated class here to see if it's one in the derived set we care about.
const FString ClassFromData = AssetData.GetTagValueRef<FString>(FBlueprintTags::GeneratedClassPath);
if (!ClassFromData.IsEmpty())
{
2022-09-13 07:18:28 +00:00
const FTopLevelAssetPath ClassObjectPath(FPackageName::ExportTextPathToObjectPath(ClassFromData));
if (DerivedClassNames.Contains(ClassObjectPath))
2022-05-23 18:41:30 +00:00
{
UE_LOG(LogLyraEditor, Display, TEXT("\tAdding %s To Validate"), *PackageName);
BlueprintsDerivedFromNativeModifiedClasses.Emplace(AssetData);
if (BlueprintsDerivedFromNativeModifiedClasses.Num() >= GMaxAssetsChangedByAHeader)
{
bTooManyFiles = true;
UE_LOG(LogLyraEditor, Display, TEXT("Too many assets invalidated (Max %d) by change to, %s + %s"), GMaxAssetsChangedByAHeader, *ChangedHeaderModule, *ChangedHeaderReleativeToModule);
return false; // Stop enumerating.
}
}
}
}
return true;
});
}
}
// STEP 3 - Report the possibly changed blueprints as affected modified packages that need
// to be proved out.
for (const FAssetData& BlueprintsDerivedFromNativeModifiedClass : BlueprintsDerivedFromNativeModifiedClasses)
{
OutChangedPackageNames.Add(BlueprintsDerivedFromNativeModifiedClass.PackageName.ToString());
}
}
}
#undef LOCTEXT_NAMESPACE