283 lines
10 KiB
C#
283 lines
10 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text.Json;
|
|
using EpicGames.Core;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace EpicGames.Horde.Issues.Handlers
|
|
{
|
|
/// <summary>
|
|
/// Instance of an issue handler for localization warnings/errors
|
|
/// </summary>
|
|
[IssueHandler]
|
|
public class LocalizationIssueHandler : IssueHandler
|
|
{
|
|
private class LocalizationData
|
|
{
|
|
public string _file = "";
|
|
public int _line = -1;
|
|
public string _location = ""; // This can be {pathToFile}({line}), just {pathToFile} or multiple other possibilities including {notAFile}. Let's never assume it is easy to parse.
|
|
public string _localizationNamespace = "";
|
|
public string _localizationKey = "";
|
|
|
|
public string _conflictFile = "";
|
|
public int _conflictLine = -1;
|
|
public string _conflictLocation = ""; // This can be {pathToFile}({line}), just {pathToFile} or multiple other possibilities including {notAFile}. Let's never assume it is easy to parse.
|
|
public string _conflictLocalizationNamespace = "";
|
|
public string _conflictLocalizationKey = "";
|
|
|
|
public bool HasLocalizationKey() { return !System.String.IsNullOrEmpty(_localizationKey); }
|
|
public string GetNamespaceKeyString() { return HasLocalizationKey() ? _localizationNamespace + "," + _localizationKey : ""; }
|
|
|
|
public bool HasFile() { return !System.String.IsNullOrEmpty(_file); }
|
|
public bool HasConflictFile() { return !System.String.IsNullOrEmpty(_conflictFile); }
|
|
public bool HasAnyFile() { return HasFile() || HasConflictFile(); }
|
|
}
|
|
|
|
readonly IssueHandlerContext _context;
|
|
readonly List<IssueEventGroup> _issues = new List<IssueEventGroup>();
|
|
readonly IssueEventGroup _issuesWithoutClearFiles = new IssueEventGroup("Localization", "Localization {Severity}", IssueChangeFilter.All);
|
|
|
|
/// <inheritdoc/>
|
|
public override int Priority => 10;
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public LocalizationIssueHandler(IssueHandlerContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the given event id matches
|
|
/// </summary>
|
|
/// <param name="eventId">The event id to compare</param>
|
|
/// <returns>True if the given event id matches</returns>
|
|
public static bool IsMatchingEventId(EventId? eventId)
|
|
{
|
|
return eventId == KnownLogEvents.Engine_Localization;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if an event should be masked by this
|
|
/// </summary>
|
|
/// <param name="eventId"></param>
|
|
/// <returns></returns>
|
|
static bool IsMaskedEventId(EventId eventId)
|
|
{
|
|
return eventId == KnownLogEvents.ExitCode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate all the IssueKeys with the extracted LocalizationData
|
|
/// </summary>
|
|
/// <param name="keys">Set of keys</param>
|
|
/// <param name="data">The localization data</param>
|
|
private static void AddLocalizationIssueKeys(HashSet<IssueKey> keys, LocalizationData data)
|
|
{
|
|
if(data.HasFile())
|
|
{
|
|
keys.AddSourceFile(data._file, IssueKeyType.File);
|
|
}
|
|
|
|
if (data.HasConflictFile())
|
|
{
|
|
keys.AddSourceFile(data._conflictFile, IssueKeyType.File);
|
|
}
|
|
|
|
if (data.HasLocalizationKey())
|
|
{
|
|
keys.Add(new IssueKey(data.GetNamespaceKeyString(), IssueKeyType.None));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extracts the localization data from the properties of an IssueEvent
|
|
/// </summary>
|
|
/// <param name="issueEvent">The event data</param>
|
|
private static LocalizationData GetLocalizationData(IssueEvent issueEvent)
|
|
{
|
|
LocalizationData data = new LocalizationData();
|
|
|
|
foreach (JsonLogEvent line in issueEvent.Lines)
|
|
{
|
|
JsonDocument document = JsonDocument.Parse(line.Data);
|
|
|
|
JsonElement properties;
|
|
if (document.RootElement.TryGetProperty("properties", out properties) && properties.ValueKind == JsonValueKind.Object)
|
|
{
|
|
foreach (JsonProperty property in properties.EnumerateObject())
|
|
{
|
|
if (property.NameEquals("file") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._file = property.Value.GetString()!;
|
|
}
|
|
else if (property.NameEquals("line") && property.Value.ValueKind == JsonValueKind.Number)
|
|
{
|
|
data._line = property.Value.GetInt32();
|
|
}
|
|
else if (property.NameEquals("location") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._location = property.Value.GetString()!;
|
|
}
|
|
else if (property.NameEquals("locNamespace") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._localizationNamespace = property.Value.GetString()!;
|
|
}
|
|
// "locID" is deprecated and will be removed soon
|
|
else if ((property.NameEquals("locKey") || property.NameEquals("locID")) && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._localizationKey = property.Value.GetString()!;
|
|
}
|
|
else if (property.NameEquals("conflictFile") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._conflictFile = property.Value.GetString()!;
|
|
}
|
|
else if (property.NameEquals("conflictLine") && property.Value.ValueKind == JsonValueKind.Number)
|
|
{
|
|
data._conflictLine = property.Value.GetInt32();
|
|
}
|
|
else if (property.NameEquals("conflictLocation") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._conflictLocation = property.Value.GetString()!;
|
|
}
|
|
else if (property.NameEquals("conflictLocNamespace") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._conflictLocalizationNamespace = property.Value.GetString()!;
|
|
}
|
|
else if (property.NameEquals("conflictLocKey") && property.Value.ValueKind == JsonValueKind.String)
|
|
{
|
|
data._conflictLocalizationKey = property.Value.GetString()!;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ConvertLocationToFileAndLineNumber(data);
|
|
RemoveFilesExtension(data);
|
|
|
|
return data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Location data in LocalizationData may contain file and line information. We can try to parse it.
|
|
/// </summary>
|
|
/// <param name="data">The localization data</param>
|
|
private static void ConvertLocationToFileAndLineNumber(LocalizationData data)
|
|
{
|
|
if (!System.String.IsNullOrEmpty(data._location))
|
|
{
|
|
// If the TextLocation contains a '(', we assume it is a format: "/Path/To/File.cpp(10)" where 10 is a line number that can be parsed
|
|
int startLineIndex = data._location.IndexOf('(', System.StringComparison.Ordinal);
|
|
// If the TextLocation contains a '.', we assume it is a format: "/Path/To/Filename.*" where the filename can be retrieved
|
|
int startExtensionIndex = data._location.IndexOf('.', System.StringComparison.Ordinal);
|
|
if (startLineIndex > 0 && data._line == -1)
|
|
{
|
|
data._line = Int32.Parse(data._location.Substring(startLineIndex + 1, data._location.Length - startLineIndex - 2));
|
|
}
|
|
if (startExtensionIndex > 0 && System.String.IsNullOrEmpty(data._file))
|
|
{
|
|
data._file = data._location.Substring(0, startExtensionIndex);
|
|
}
|
|
}
|
|
if (!System.String.IsNullOrEmpty(data._conflictLocation))
|
|
{
|
|
// If the TextLocation contains a '(', we assume it is a format: "/Path/To/File.cpp(10)" where 10 is a line number that can be parsed
|
|
int startLineIndex = data._conflictLocation.IndexOf('(', System.StringComparison.Ordinal);
|
|
// If the TextLocation contains a '.', we assume it is a format: "/Path/To/Filename.*" where the filename can be retrieved
|
|
int startExtensionIndex = data._conflictLocation.IndexOf('.', System.StringComparison.Ordinal);
|
|
if (startLineIndex > 0 && data._conflictLine == -1)
|
|
{
|
|
data._conflictLine = Int32.Parse(data._conflictLocation.Substring(startLineIndex + 1, data._conflictLocation.Length - startLineIndex - 2));
|
|
}
|
|
if (startExtensionIndex > 0 && System.String.IsNullOrEmpty(data._conflictFile))
|
|
{
|
|
data._conflictFile = data._conflictLocation.Substring(0, startExtensionIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// LocalizationData file paths may contain extensions, remove them
|
|
/// </summary>
|
|
/// <param name="data">The localization data</param>
|
|
private static void RemoveFilesExtension(LocalizationData data)
|
|
{
|
|
if (!System.String.IsNullOrEmpty(data._file))
|
|
{
|
|
// If the File path contains a '.', we assume it is a format: "/Path/To/Filename.extension" where the filename can be retrieved
|
|
int startExtensionIndex = data._file.IndexOf('.', System.StringComparison.Ordinal);
|
|
if (startExtensionIndex > 0)
|
|
{
|
|
data._file = data._file.Substring(0, startExtensionIndex);
|
|
}
|
|
}
|
|
if (!System.String.IsNullOrEmpty(data._conflictFile))
|
|
{
|
|
// If the ConflictFile contains a '.', we assume it is a format: "/Path/To/Filename.extension" where the filename can be retrieved
|
|
int startExtensionIndex = data._conflictFile.IndexOf('.', System.StringComparison.Ordinal);
|
|
if (startExtensionIndex > 0)
|
|
{
|
|
data._conflictFile = data._conflictFile.Substring(0, startExtensionIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override bool HandleEvent(IssueEvent issueEvent)
|
|
{
|
|
if (issueEvent.EventId != null)
|
|
{
|
|
EventId eventId = issueEvent.EventId.Value;
|
|
if (IsMatchingEventId(eventId))
|
|
{
|
|
LocalizationData data = GetLocalizationData(issueEvent);
|
|
|
|
IssueEventGroup issueGroup;
|
|
if(data.HasAnyFile())
|
|
{
|
|
// Create a new issue group (might be grouped later based on keys)
|
|
string localizationIssueGroup = "Localization";
|
|
string localizationSummaryTemplate = (!System.String.IsNullOrEmpty(_context.NodeName) ? "{Meta:Node}: " : "") + "Localization {Severity} in {Files}";
|
|
issueGroup = new IssueEventGroup(localizationIssueGroup, localizationSummaryTemplate, IssueChangeFilter.All);
|
|
if (!System.String.IsNullOrEmpty(_context.NodeName))
|
|
{
|
|
issueGroup.Metadata.Add("Node", _context.NodeName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If we have no idea on the file an issue is associated with, then we won't be able to narrow down a suspect.
|
|
// If we can't narrow down a suspect. There is no gain to split the issues amongst different issueGroup.
|
|
// Actually, there is only a risk of creating thousands of issue group if localization is in a weird/broken state.
|
|
issueGroup = _issuesWithoutClearFiles;
|
|
}
|
|
|
|
// Add current event to the chosen issueGroup
|
|
issueGroup.Events.Add(issueEvent);
|
|
|
|
AddLocalizationIssueKeys(issueGroup.Keys, data);
|
|
|
|
if(issueGroup.Keys.Count > 0 && !_issues.Contains(issueGroup))
|
|
{
|
|
_issues.Add(issueGroup);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
else if (_issues.Count > 0 && IsMaskedEventId(eventId))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override IEnumerable<IssueEventGroup> GetIssues() => _issues;
|
|
}
|
|
}
|