2022-05-23 18:41:30 +00:00
// Copyright Epic Games, Inc. All Rights Reserved.
# include "EditorValidator.h"
2022-08-24 15:29:22 +00:00
# include "AssetData.h"
# include "AssetRegistryModule.h"
2022-05-23 18:41:30 +00:00
# include "Logging/MessageLog.h"
# include "MessageLogModule.h"
# include "Misc/ConfigCacheIni.h"
# include "Misc/ScopedSlowTask.h"
# include "Misc/MessageDialog.h"
# include "EditorValidatorSubsystem.h"
# include "Editor.h"
# include "ISourceControlModule.h"
# include "ISourceControlProvider.h"
# include "SourceControlHelpers.h"
# include "SourceControlOperations.h"
# include "StudioAnalytics.h"
# include "ShaderCompiler.h"
# include "Misc/ConfigCacheIni.h"
# include "Settings/ProjectPackagingSettings.h"
# include "SourceCodeNavigation.h"
# include "Stats/StatsMisc.h"
# include "DataValidationModule.h"
# include "LyraEditor.h"
# 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 \n See 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 ( ) )
{
UE_LOG ( LogLyraEditor , Display , TEXT ( " Preloading %s... " ) , * AssetToCheck . ObjectPath . ToString ( ) ) ;
// 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-08-24 15:29:22 +00:00
TSet < FName > DerivedClassNames ;
TArray < FName > ClassNames ;
ClassNames . Add ( ModifiedClass - > GetFName ( ) ) ;
AssetRegistry . GetDerivedClassNames ( ClassNames , TSet < FName > ( ) , 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-08-24 15:29:22 +00:00
Filter . ClassNames . Add ( UBlueprintCore : : StaticClass ( ) - > GetFName ( ) ) ;
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 ( ) )
{
const FString ClassObjectPath = FPackageName : : ExportTextPathToObjectPath ( ClassFromData ) ;
const FString ClassName = FPackageName : : ObjectPathToObjectName ( ClassObjectPath ) ;
2022-08-24 15:29:22 +00:00
if ( DerivedClassNames . Contains ( FName ( * ClassName ) ) )
2022-05-23 18:41:30 +00:00
{
UE_LOG ( LogLyraEditor , Display , TEXT ( " \t Adding %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