Replace all raw string comparisons with StringKey, source generator that creates cache keys for all names for Gen7 plugin
All checks were successful
Build / Build (push) Successful in 2m21s

This commit is contained in:
2026-08-27 18:30:57 +02:00
parent 3a58f55bbf
commit 942be8eaeb
160 changed files with 1035 additions and 491 deletions

View File

@@ -0,0 +1,196 @@
using System.Text.Json;
namespace PkmnLib.Plugin.Gen7.SourceGen.Parsing;
/// <summary>
/// A single generated constants class: its name and the raw data names it should contain.
/// </summary>
public sealed record ClassSpec(string ClassName, string SourceFile, EquatableArray<string> Names);
/// <summary>
/// A diagnostic produced during extraction, kept as plain data so pipeline values stay equatable.
/// </summary>
public sealed record ExtractionDiagnostic(string Id, string Message);
/// <summary>
/// The result of extracting all constant classes from a single data file.
/// </summary>
public sealed record ExtractionResult(
EquatableArray<ClassSpec> Classes,
EquatableArray<ExtractionDiagnostic> Diagnostics);
/// <summary>
/// Extracts the names for the generated constant classes from the Gen 7 data files.
/// </summary>
public static class DataExtractor
{
private static readonly HashSet<string> KnownFiles = new(StringComparer.Ordinal)
{
"Moves.jsonc", "Abilities.jsonc", "Items.json", "Pokemon.json", "Types.csv", "Natures.csv",
};
/// <summary>
/// Matches the runtime's <c>JsonOptions</c>: the data files contain <c>//</c> comments
/// (including Items.json, despite its extension) and may contain trailing commas.
/// </summary>
private static readonly JsonDocumentOptions JsonOptions = new()
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
public static bool IsKnownFile(string fileName) => KnownFiles.Contains(fileName);
/// <summary>
/// Extracts the constant classes for a data file. Never throws: a generator that throws is
/// silently disabled by IDE hosts, so failures are reported as diagnostics instead.
/// </summary>
public static ExtractionResult Extract(string fileName, string? text)
{
if (text is null)
{
return Error(fileName, "file content could not be read");
}
try
{
return fileName switch
{
"Moves.jsonc" => ExtractMoves(fileName, text),
"Abilities.jsonc" => ExtractTopLevelKeys(fileName, text, "AbilityNames"),
"Items.json" => ExtractItems(fileName, text),
"Pokemon.json" => ExtractSpecies(fileName, text),
"Types.csv" => ExtractTypes(fileName, text),
"Natures.csv" => ExtractNatures(fileName, text),
_ => new ExtractionResult(new EquatableArray<ClassSpec>([]),
new EquatableArray<ExtractionDiagnostic>([])),
};
}
catch (Exception e)
{
return Error(fileName, e.Message);
}
}
private static ExtractionResult Error(string fileName, string message) =>
new(new EquatableArray<ClassSpec>([]), new EquatableArray<ExtractionDiagnostic>([
new ExtractionDiagnostic("PKMN7G000", $"Failed to parse data file '{fileName}': {message}"),
]));
private static ExtractionResult ExtractMoves(string fileName, string text)
{
var moveNames = new HashSet<string>(StringComparer.Ordinal);
var flags = new HashSet<string>(StringComparer.Ordinal);
var effectNames = new HashSet<string>(StringComparer.Ordinal);
using var doc = JsonDocument.Parse(text, JsonOptions);
foreach (var move in doc.RootElement.GetProperty("data").EnumerateArray())
{
Add(moveNames, move.GetProperty("name").GetString());
if (move.TryGetProperty("flags", out var flagsElement))
{
foreach (var flag in flagsElement.EnumerateArray())
{
Add(flags, flag.GetString());
}
}
if (move.TryGetProperty("effect", out var effect) && effect.TryGetProperty("name", out var effectName))
{
Add(effectNames, effectName.GetString());
}
}
return Result(Class("MoveNames", fileName, moveNames), Class("MoveFlags", fileName, flags),
Class("MoveEffectNames", fileName, effectNames));
}
private static ExtractionResult ExtractTopLevelKeys(string fileName, string text, string className)
{
var names = new HashSet<string>(StringComparer.Ordinal);
using var doc = JsonDocument.Parse(text, JsonOptions);
foreach (var property in doc.RootElement.EnumerateObject())
{
if (!property.Name.StartsWith("$", StringComparison.Ordinal))
Add(names, property.Name);
}
return Result(Class(className, fileName, names));
}
private static ExtractionResult ExtractItems(string fileName, string text)
{
var names = new HashSet<string>(StringComparer.Ordinal);
using var doc = JsonDocument.Parse(text, JsonOptions);
foreach (var item in doc.RootElement.EnumerateArray())
{
Add(names, item.GetProperty("name").GetString());
}
return Result(Class("ItemNames", fileName, names));
}
private static ExtractionResult ExtractSpecies(string fileName, string text)
{
var speciesNames = new HashSet<string>(StringComparer.Ordinal);
var formNames = new HashSet<string>(StringComparer.Ordinal);
using var doc = JsonDocument.Parse(text, JsonOptions);
foreach (var property in doc.RootElement.EnumerateObject())
{
if (property.Name.StartsWith("$", StringComparison.Ordinal))
continue;
Add(speciesNames, property.Name);
if (property.Value.TryGetProperty("formes", out var formes))
{
foreach (var forme in formes.EnumerateObject())
{
Add(formNames, forme.Name);
}
}
}
return Result(Class("SpeciesNames", fileName, speciesNames), Class("FormNames", fileName, formNames));
}
private static ExtractionResult ExtractTypes(string fileName, string text)
{
var names = new HashSet<string>(StringComparer.Ordinal);
var header = FirstLine(text);
var cells = header.Split(header.Contains('|') ? '|' : ',');
for (var i = 1; i < cells.Length; i++)
{
Add(names, cells[i]);
}
return Result(Class("TypeNames", fileName, names));
}
private static ExtractionResult ExtractNatures(string fileName, string text)
{
var names = new HashSet<string>(StringComparer.Ordinal);
var lines = text.Split('\n');
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (line.Length == 0)
continue;
Add(names, line.Split(line.Contains('|') ? '|' : ',')[0]);
}
return Result(Class("NatureNames", fileName, names));
}
private static string FirstLine(string text)
{
var index = text.IndexOf('\n');
return (index < 0 ? text : text.Substring(0, index)).Trim();
}
private static void Add(HashSet<string> names, string? raw)
{
var name = raw?.Trim().ToLowerInvariant();
if (!string.IsNullOrEmpty(name))
names.Add(name!);
}
private static ClassSpec Class(string className, string fileName, HashSet<string> names)
{
var sorted = names.ToArray();
Array.Sort(sorted, StringComparer.Ordinal);
return new ClassSpec(className, fileName, new EquatableArray<string>(sorted));
}
private static ExtractionResult Result(params ClassSpec[] classes) =>
new(new EquatableArray<ClassSpec>(classes), new EquatableArray<ExtractionDiagnostic>([]));
}