Skip to content

Commit

Permalink
Introduced LintResults Object
Browse files Browse the repository at this point in the history
Collects the information collected during a lint run in a confined Object.
Centralized JSON and HTML Generation
  • Loading branch information
jwindgassen committed Feb 28, 2024
1 parent 3f09b15 commit 5b6f6ad
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 207 deletions.
2 changes: 1 addition & 1 deletion Source/Linter/Private/LintRule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ TArray<FLintRuleViolation> FLintRuleViolation::AllRuleViolationsOfRuleGroup(cons
TArray<UObject*> FLintRuleViolation::AllRuleViolationViolators(const TArray<FLintRuleViolation>& RuleViolationCollection) {
TArray<UObject*> Violators;
for (const FLintRuleViolation& RuleViolation : RuleViolationCollection) {
Violators.Add(RuleViolation.Violator.Get());
Violators.AddUnique(RuleViolation.Violator.Get());
}
return Violators;
}
Expand Down
153 changes: 131 additions & 22 deletions Source/Linter/Private/LintRuleSet.cpp
Original file line number Diff line number Diff line change
@@ -1,28 +1,135 @@
#include "LintRuleSet.h"

#include "AnyObject_LinterDummyClass.h"
#include "IPluginManager.h"
#include "JsonObjectWrapper.h"
#include "LintRunner.h"
#include "Linter.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Modules/ModuleManager.h"
#include "HAL/RunnableThread.h"

ULintRuleSet::ULintRuleSet(const FObjectInitializer& ObjectInitializer) :
Super(ObjectInitializer) {}

TArray<TSharedPtr<FLintRuleViolation>> ULintResults::GetSharedViolations() const {
TArray<TSharedPtr<FLintRuleViolation>> SharedRuleViolations;
for (const FLintRuleViolation& Violation : Violations) {
TSharedPtr<FLintRuleViolation> SharedViolation = MakeShared<FLintRuleViolation>(Violation);
SharedViolation->PopulateAssetData();
SharedRuleViolations.Push(SharedViolation);
}

return SharedRuleViolations;
}

TSharedPtr<FJsonObject> ULintResults::GenerateJsonReport() const {
auto Report = MakeShared<FJsonObject>();

Report->SetStringField("Project", FPaths::GetBaseFilename(FPaths::GetProjectFilePath()));
Report->SetStringField("Result", Result.ToString());
Report->SetNumberField("Warnings", Warnings);
Report->SetNumberField("Errors", Errors);

TArray<TSharedPtr<FJsonValue>> PathArray;
for (const auto& Path : Paths) {
PathArray.Add(MakeShared<FJsonValueString>(Path));
}
Report->SetArrayField("Paths", PathArray);

TArray<TSharedPtr<FJsonValue>> AssetsArray;
for (const auto& Asset : CheckedAssets) {
#if UE_VERSION_NEWER_THAN(5, 1, 0)
AssetsArray.Add(MakeShared<FJsonValueString>(Asset.GetObjectPathString()));
#else
AssetsArray.Add(MakeShared<FJsonValueString>(Asset.ObjectPath.ToString()));
#endif
}
Report->SetArrayField("CheckedAssets", AssetsArray);

TArray<TSharedPtr<FJsonValue>> ViolationsArray;
for (const UObject* Violator : FLintRuleViolation::AllRuleViolationViolators(Violations)) {
TSharedPtr<FJsonObject> ViolationObject = MakeShareable(new FJsonObject);

FAssetData AssetData;
TArray<FLintRuleViolation> ViolatorViolations = FLintRuleViolation::AllRuleViolationsWithViolator(Violations, Violator);

if (ViolatorViolations.Num() > 0) {
ViolatorViolations[0].PopulateAssetData();
AssetData = ViolatorViolations[0].ViolatorAssetData;

ViolationObject->SetStringField("AssetName", AssetData.AssetName.ToString());
ViolationObject->SetStringField("AssetFullName", AssetData.GetFullName());
#if UE_VERSION_NEWER_THAN(5, 1, 0)
ViolationObject->SetStringField("AssetPath", AssetData.GetObjectPathString());
#else
ViolationObject->SetStringField("ViolatorAssetPath", AssetData.ObjectPath.ToString());
#endif
//@TODO: Thumbnail export?

TArray<TSharedPtr<FJsonValue>> ViolationObjects;
for (const FLintRuleViolation& Violation : ViolatorViolations) {
ULintRule* LintRule = Violation.ViolatedRule->GetDefaultObject<ULintRule>();
check(LintRule != nullptr);

TSharedPtr<FJsonObject> RuleJsonObject = MakeShareable(new FJsonObject);
RuleJsonObject->SetStringField("Group", LintRule->RuleGroup.ToString());
RuleJsonObject->SetStringField("Title", LintRule->RuleTitle.ToString());
RuleJsonObject->SetStringField("Description", LintRule->RuleDescription.ToString());
RuleJsonObject->SetStringField("RuleURL", LintRule->RuleURL);
RuleJsonObject->SetNumberField("Severity", static_cast<int32>(LintRule->RuleSeverity));
RuleJsonObject->SetStringField("RecommendedAction", Violation.RecommendedAction.ToString());

ViolationObjects.Add(MakeShared<FJsonValueObject>(RuleJsonObject));
}

ViolationObject->SetArrayField("Violations", ViolationObjects);
}

ViolationsArray.Add(MakeShared<FJsonValueObject>(ViolationObject));
}
Report->SetArrayField("Violators", ViolationsArray);

return Report;
}

FString ULintResults::GenerateJsonReportString() const {
const TSharedPtr<FJsonObject> Report = GenerateJsonReport();

FString ReportString;
const TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer = TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&ReportString);
FJsonSerializer::Serialize(Report.ToSharedRef(), Writer);

return ReportString;
}

FString ULintResults::GenerateHTML() const {
const FString ReportString = GenerateJsonReportString();

static const FString TemplatePath = IPluginManager::Get().FindPlugin("Linter")->GetBaseDir() / "Resources" /"LintReportTemplate.html";
UE_LOG(LogLinter, Display, TEXT("Loading HTML report template from %s"), *TemplatePath);

FString Template;
if (!FFileHelper::LoadFileToString(Template, *TemplatePath)) {
UE_LOG(LogLinter, Error, TEXT("Could not load HTML report template."));
}

Template.ReplaceInline(TEXT("{% Report %}"), *ReportString);
return Template;
}

ULinterNamingConvention* ULintRuleSet::GetNamingConvention() const {
return NamingConvention.Get();
}

TArray<FLintRuleViolation> ULintRuleSet::LintPath(TArray<FString> AssetPaths, FScopedSlowTask* ParentScopedSlowTask /*= nullptr*/) const {
ULintResults* ULintRuleSet::LintPath(TArray<FString> AssetPaths, FScopedSlowTask* ParentScopedSlowTask /*= nullptr*/) const {
// ReSharper disable once CppExpressionWithoutSideEffects
NamingConvention.LoadSynchronous();

TArray<FLintRuleViolation> RuleViolations;
ULintResults* Results = NewObject<ULintResults>();

if (AssetPaths.Num() == 0) {
AssetPaths.Push(TEXT("/Game"));
}
Results->Paths = AssetPaths;

// Begin loading assets
const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
Expand All @@ -31,8 +138,6 @@ TArray<FLintRuleViolation> ULintRuleSet::LintPath(TArray<FString> AssetPaths, FS
AssetRegistryModule.Get().SearchAllAssets(/*bSynchronousSearch =*/true);
UE_LOG(LogLinter, Display, TEXT("Finished loading the asset registry. Loading assets..."));

TArray<FAssetData> AssetList;

FARFilter ARFilter;
ARFilter.bRecursivePaths = true;

Expand All @@ -41,23 +146,23 @@ TArray<FLintRuleViolation> ULintRuleSet::LintPath(TArray<FString> AssetPaths, FS
ARFilter.PackagePaths.Push(FName(*AssetPath));
}

AssetRegistryModule.Get().GetAssets(ARFilter, AssetList);
AssetRegistryModule.Get().GetAssets(ARFilter, Results->CheckedAssets);

TArray<FLintRunner*> LintRunners;
TArray<FRunnableThread*> Threads;

if (ParentScopedSlowTask != nullptr) {
ParentScopedSlowTask->TotalAmountOfWork = AssetList.Num() + 2;
ParentScopedSlowTask->TotalAmountOfWork = Results->CheckedAssets.Num() + 2;
ParentScopedSlowTask->CompletedWork = 0.0f;
}

for (const FAssetData& Asset : AssetList) {
for (const FAssetData& Asset : Results->CheckedAssets) {
check(Asset.IsValid());
UE_LOG(LogLinter, Verbose, TEXT("Creating Lint Thread for asset \"%s\"."), *Asset.AssetName.ToString());
UObject* Object = Asset.GetAsset();
check(Object != nullptr);

FLintRunner* Runner = new FLintRunner(Object, this, &RuleViolations, ParentScopedSlowTask);
FLintRunner* Runner = new FLintRunner(Object, this, &Results->Violations, ParentScopedSlowTask);
check(Runner != nullptr);

LintRunners.Add(Runner);
Expand Down Expand Up @@ -88,20 +193,24 @@ TArray<FLintRuleViolation> ULintRuleSet::LintPath(TArray<FString> AssetPaths, FS
ParentScopedSlowTask->EnterProgressFrame(1.0f, NSLOCTEXT("Linter", "ScanTaskFinished", "Tabulating Data..."));
}

return RuleViolations;
}

TArray<TSharedPtr<FLintRuleViolation>> ULintRuleSet::LintPathShared(const TArray<FString> AssetPaths, FScopedSlowTask* ParentScopedSlowTask /*= nullptr*/) const {
TArray<FLintRuleViolation> RuleViolations = LintPath(AssetPaths, ParentScopedSlowTask);

TArray<TSharedPtr<FLintRuleViolation>> SharedRuleViolations;
for (const FLintRuleViolation& Violation : RuleViolations) {
TSharedPtr<FLintRuleViolation> SharedViolation = MakeShared<FLintRuleViolation>(Violation);
SharedViolation->PopulateAssetData();
SharedRuleViolations.Push(SharedViolation);
// Count Errors and Warnings
for (const FLintRuleViolation& Violation : Results->Violations) {
if (Violation.ViolatedRule->GetDefaultObject<ULintRule>()->RuleSeverity <= ELintRuleSeverity::Error) {
Results->Errors++;
} else {
Results->Warnings++;
}
}

return SharedRuleViolations;
// Generate Result String
Results->Result = FText::FormatNamed(
FText::FromString("Linted {NumAssets} Assets: {NumWarnings} {NumWarnings}|plural(one=warning,other=warnings), {NumErrors} {NumErrors}|plural(one=error,other=errors)."),
TEXT("NumAssets"), FText::FromString(FString::FromInt(Results->CheckedAssets.Num())),
TEXT("NumWarnings"), FText::FromString(FString::FromInt(Results->Warnings)),
TEXT("NumErrors"), FText::FromString(FString::FromInt(Results->Warnings))
);

return Results;
}

const FLintRuleList* ULintRuleSet::GetLintRuleListForClass(const TSoftClassPtr<UObject> Class) const {
Expand Down
110 changes: 16 additions & 94 deletions Source/Linter/Private/LinterCommandlet.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2020 Gamemakin LLC. All Rights Reserved.

#include "LinterCommandlet.h"

#include "AssetRegistry/AssetRegistryModule.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
Expand Down Expand Up @@ -72,107 +73,42 @@ int32 ULinterCommandlet::Main(const FString& InParams) {
UE_LOG(LinterCommandlet, Error, TEXT("Failed to load a rule set. Aborting. Returning error code 1."));
return 1;
}

UE_LOG(LinterCommandlet, Display, TEXT("Using rule set: %s"), *RuleSet->GetFullName());

if (Paths.Num() == 0) {
Paths.Add(TEXT("/Game"));
}

UE_LOG(LinterCommandlet, Display, TEXT("Attempting to Lint paths: %s"), *FString::Join(Paths, TEXT(", ")));

const TArray<FLintRuleViolation> RuleViolations = RuleSet->LintPath(Paths);

int32 NumErrors = 0;
int32 NumWarnings = 0;

for (const FLintRuleViolation& Violation : RuleViolations) {
if (Violation.ViolatedRule->GetDefaultObject<ULintRule>()->RuleSeverity <= ELintRuleSeverity::Error) {
NumErrors++;
} else {
NumWarnings++;
}
}

FString ResultsString = FText::FormatNamed(FText::FromString("Lint completed with {NumWarnings} {NumWarnings}|plural(one=warning,other=warnings), {NumErrors} {NumErrors}|plural(one=error,other=errors)."), TEXT("NumWarnings"), FText::FromString(FString::FromInt(NumWarnings)), TEXT("NumErrors"), FText::FromString(FString::FromInt(NumErrors))).ToString();
UE_LOG(LinterCommandlet, Display, TEXT("Lint completed with %s."), *ResultsString);
const ULintResults* LintResults = RuleSet->LintPath(Paths);
UE_LOG(LinterCommandlet, Display, TEXT("%s"), *LintResults->Result.ToString());

bool bWriteReport = Switches.Contains(TEXT("json")) || ParamsMap.Contains(TEXT("json")) || Switches.Contains(TEXT("html")) || ParamsMap.Contains(TEXT("html"));
if (bWriteReport) {
if (Switches.Contains("json") || ParamsMap.Contains("json") || Switches.Contains("html") || ParamsMap.Contains("html")) {
UE_LOG(LinterCommandlet, Display, TEXT("Generating output report..."));

TSharedPtr<FJsonObject> RootJsonObject = MakeShareable(new FJsonObject);
TArray<TSharedPtr<FJsonValue>> ViolatorJsonObjects;

TArray<UObject*> UniqueViolators = FLintRuleViolation::AllRuleViolationViolators(RuleViolations);
for (const UObject* Violator : UniqueViolators) {
TSharedPtr<FJsonObject> AssetJsonObject = MakeShareable(new FJsonObject);
TArray<FLintRuleViolation> UniqueViolatorViolations = FLintRuleViolation::AllRuleViolationsWithViolator(RuleViolations, Violator);

FAssetData AssetData;
if (UniqueViolatorViolations.Num() > 0) {
UniqueViolatorViolations[0].PopulateAssetData();
AssetData = UniqueViolatorViolations[0].ViolatorAssetData;
AssetJsonObject->SetStringField(TEXT("ViolatorAssetName"), AssetData.AssetName.ToString());
#if UE_VERSION_NEWER_THAN(5, 1, 0)
AssetJsonObject->SetStringField(TEXT("ViolatorAssetPath"), AssetData.GetObjectPathString());
#else
AssetJsonObject->SetStringField(TEXT("ViolatorAssetPath"), AssetData.ObjectPath.ToString());
#endif
AssetJsonObject->SetStringField(TEXT("ViolatorFullName"), AssetData.GetFullName());
//@TODO: Thumbnail export?

TArray<TSharedPtr<FJsonValue>> RuleViolationJsonObjects;

for (const FLintRuleViolation& Violation : UniqueViolatorViolations) {
ULintRule* LintRule = Violation.ViolatedRule->GetDefaultObject<ULintRule>();
check(LintRule != nullptr);

TSharedPtr<FJsonObject> RuleJsonObject = MakeShareable(new FJsonObject);
RuleJsonObject->SetStringField(TEXT("RuleGroup"), LintRule->RuleGroup.ToString());
RuleJsonObject->SetStringField(TEXT("RuleTitle"), LintRule->RuleTitle.ToString());
RuleJsonObject->SetStringField(TEXT("RuleDesc"), LintRule->RuleDescription.ToString());
RuleJsonObject->SetStringField(TEXT("RuleURL"), LintRule->RuleURL);
RuleJsonObject->SetNumberField(TEXT("RuleSeverity"), static_cast<int32>(LintRule->RuleSeverity));
RuleJsonObject->SetStringField(TEXT("RuleRecommendedAction"), Violation.RecommendedAction.ToString());
RuleViolationJsonObjects.Push(MakeShareable(new FJsonValueObject(RuleJsonObject)));
}

AssetJsonObject->SetArrayField(TEXT("Violations"), RuleViolationJsonObjects);
}

ViolatorJsonObjects.Add(MakeShareable(new FJsonValueObject(AssetJsonObject)));
}

// Save off our JSON to a string
RootJsonObject->SetArrayField(TEXT("Violators"), ViolatorJsonObjects);
FString JsonReport;
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer = TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonReport);
FJsonSerializer::Serialize(RootJsonObject.ToSharedRef(), Writer);

// write json file if requested
if (Switches.Contains(TEXT("json")) || ParamsMap.Contains(FString(TEXT("json")))) {
// Write JSON file if requested
if (Switches.Contains("json") || ParamsMap.Contains(FString("json"))) {
FDateTime Now = FDateTime::Now();
FString JsonOutputName = TEXT("lint-report-") + Now.ToString() + TEXT(".json");
FString JsonOutputName = "lint-report-" + Now.ToString() + ".json";

const FString LintReportPath = FPaths::ProjectSavedDir() / TEXT("LintReports");
const FString LintReportPath = FPaths::ProjectSavedDir() / "LintReports";
FString FullOutputPath = LintReportPath / JsonOutputName;

if (ParamsMap.Contains(FString(TEXT("json")))) {
const FString JsonOutputOverride = *ParamsMap.FindChecked(FString(TEXT("json")));
if (ParamsMap.Contains("json")) {
const FString JsonOutputOverride = *ParamsMap.FindChecked(FString("json"));
if (FPaths::IsRelative(JsonOutputOverride)) {
JsonOutputName = JsonOutputOverride;
FullOutputPath = LintReportPath / JsonOutputName;
FullOutputPath = LintReportPath / JsonOutputOverride;
} else {
FullOutputPath = JsonOutputOverride;
}
}

FullOutputPath = FPaths::ConvertRelativePathToFull(FullOutputPath);
IFileManager::Get().MakeDirectory(*FPaths::GetPath(FullOutputPath), true);

UE_LOG(LinterCommandlet, Display, TEXT("Exporting JSON report to %s"), *FullOutputPath);
if (FFileHelper::SaveStringToFile(JsonReport, *FullOutputPath)) {

const FString ReportString = LintResults->GenerateJsonReportString();
if (FFileHelper::SaveStringToFile(ReportString, *FullOutputPath)) {
UE_LOG(LinterCommandlet, Display, TEXT("Exported JSON report successfully."));
} else {
UE_LOG(LinterCommandlet, Error, TEXT("Failed to export JSON report. Aborting. Returning error code 1."));
Expand Down Expand Up @@ -202,21 +138,7 @@ int32 ULinterCommandlet::Main(const FString& InParams) {
IFileManager::Get().MakeDirectory(*FPaths::GetPath(FullOutputPath), true);
UE_LOG(LinterCommandlet, Display, TEXT("Exporting HTML report to %s"), *FullOutputPath);

FString TemplatePath = FPaths::Combine(*IPluginManager::Get().FindPlugin(TEXT("Linter"))->GetBaseDir(), TEXT("Resources"), TEXT("LintReportTemplate.html"));
UE_LOG(LinterCommandlet, Display, TEXT("Loading HTML report template from %s"), *TemplatePath);

FString HTMLReport;
if (FFileHelper::LoadFileToString(HTMLReport, *TemplatePath)) {
UE_LOG(LinterCommandlet, Display, TEXT("Loading HTML report template successfully."));

HTMLReport.ReplaceInline(TEXT("{% TITLE %}"), *FPaths::GetBaseFilename(FPaths::GetProjectFilePath()));
HTMLReport.ReplaceInline(TEXT("{% RESULTS %}"), *ResultsString);
HTMLReport.ReplaceInline(TEXT("{% LINT_REPORT %}"), *JsonReport);
} else {
UE_LOG(LinterCommandlet, Error, TEXT("Failed to load HTML report template."));
return 1;
}

const FString HTMLReport = LintResults->GenerateHTML();
if (FFileHelper::SaveStringToFile(HTMLReport, *FullOutputPath)) {
UE_LOG(LinterCommandlet, Display, TEXT("Exported HTML report successfully."));
} else {
Expand All @@ -226,7 +148,7 @@ int32 ULinterCommandlet::Main(const FString& InParams) {
}
}

if (NumErrors > 0 || (Switches.Contains(TEXT("TreatWarningsAsErrors")) && NumWarnings > 0)) {
if (LintResults->Errors > 0 || (Switches.Contains(TEXT("TreatWarningsAsErrors")) && LintResults->Warnings > 0)) {
UE_LOG(LinterCommandlet, Display, TEXT("Lint completed with errors. Returning error code 2."));
return 2;
}
Expand Down
Loading

0 comments on commit 5b6f6ad

Please sign in to comment.