Merge pull request #349 from sp-tarkov/json-extension-tool

Json extension tool
This commit is contained in:
Chomp
2025-06-03 21:06:19 +01:00
committed by GitHub
13 changed files with 160 additions and 243 deletions
@@ -1,30 +0,0 @@
using System.Linq;
using Mono.Cecil;
namespace JsonExtensionData.Fody;
public partial class ModuleWeaver
{
public void ProcessAssembly()
{
foreach (var type in allClasses)
{
if (!ShouldInclude(type))
{
continue;
}
if (ShouldIncludeType(type))
{
ProcessType(type);
}
}
}
public bool ShouldIncludeType(TypeDefinition type)
{
return IncludeNamespacesRegex.Any(r => r.IsMatch(type.Namespace));
}
static bool ShouldInclude(TypeDefinition type) => !type.IsSealed;
}
@@ -1,36 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
namespace JsonExtensionData.Fody;
public static class CecilExtensions
{
public static List<TypeDefinition> GetAllClasses(this ModuleDefinition moduleDefinition)
{
var definitions = new List<TypeDefinition>();
//First is always module so we will skip that;
GetTypes(moduleDefinition.Types.Skip(1), definitions);
return definitions;
}
static void GetTypes(IEnumerable<TypeDefinition> typeDefinitions, List<TypeDefinition> definitions)
{
foreach (var typeDefinition in typeDefinitions)
{
GetTypes(typeDefinition.NestedTypes, definitions);
if (typeDefinition.IsInterface)
{
continue;
}
if (typeDefinition.IsEnum)
{
continue;
}
definitions.Add(typeDefinition);
}
}
}
@@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace JsonExtensionData.Fody;
public partial class ModuleWeaver
{
public List<Regex> IncludeNamespacesRegex = new();
public void ReadConfig()
{
ReadExcludes();
}
void ReadExcludes()
{
var includeNamespacesElement = Config.Element("IncludeNamespacesRegex");
if (includeNamespacesElement != null)
{
foreach (var item in includeNamespacesElement.Value
.Split(
[
"\r\n",
"\n"
],
StringSplitOptions.RemoveEmptyEntries)
.NonEmpty())
{
IncludeNamespacesRegex.Add(new Regex(item));
}
}
}
}
@@ -1,10 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace JsonExtensionData.Fody;
public static class Extensions
{
public static IEnumerable<string> NonEmpty(this IEnumerable<string> list) =>
list.Select(_ => _.Trim()).Where(_ => _ != string.Empty);
}
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Build.props"/>
<PropertyGroup>
<!--
DO NOT change the target framework for this project
This is a Fody plugin that runs over the MSBuild runtime, changing this target framework will make this plugin
crash under certain MSBuild runtimes, like the one VS uses
-->
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FodyHelpers" Version="6.8.0"/>
</ItemGroup>
</Project>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<xs:complexType xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="IncludeNamespacesRegex"
type="xs:string"
minOccurs="0"
maxOccurs="1">
<xs:annotation>
<xs:documentation>Namespaces to use for adding JsonExtensionData properties and attribute</xs:documentation>
</xs:annotation>
</xs:element>
</xs:complexType>
@@ -1,24 +0,0 @@
using System.Collections.Generic;
using Fody;
using Mono.Cecil;
namespace JsonExtensionData.Fody;
public partial class ModuleWeaver : BaseModuleWeaver
{
List<TypeDefinition> allClasses;
public override void Execute()
{
allClasses = ModuleDefinition.GetAllClasses();
ReadConfig();
ProcessAssembly();
}
public override IEnumerable<string> GetAssembliesForScanning()
{
return [];
}
public override bool ShouldCleanReference => true;
}
@@ -1,67 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;
namespace JsonExtensionData.Fody;
public partial class ModuleWeaver
{
private TypeReference? _dictionaryStringObjectReference;
private MethodReference? _jsonExtensionDataAttributeReference;
private MethodReference? _jsonIgnoreAttributeReference;
public void ProcessType(TypeDefinition typeDefinition)
{
_dictionaryStringObjectReference ??= ModuleDefinition.ImportReference(typeof(Dictionary<string, object>));
if (_jsonExtensionDataAttributeReference is null)
{
var jsonConstructorReference = ModuleDefinition.AssemblyResolver
.Resolve(AssemblyNameReference.Parse("System.Text.Json")).MainModule
.GetType("System.Text.Json.Serialization.JsonExtensionDataAttribute").Methods
.First(m => m.IsConstructor && !m.HasParameters);
_jsonExtensionDataAttributeReference = ModuleDefinition.ImportReference(jsonConstructorReference);
}
if (_jsonIgnoreAttributeReference is null)
{
var jsonIgnoreConstructorReference = ModuleDefinition.AssemblyResolver
.Resolve(AssemblyNameReference.Parse("System.Text.Json")).MainModule
.GetType("System.Text.Json.Serialization.JsonIgnoreAttribute").Methods
.First(m => m.IsConstructor && !m.HasParameters);
_jsonIgnoreAttributeReference = ModuleDefinition.ImportReference(jsonIgnoreConstructorReference);
}
var propertyDefinition = new PropertyDefinition("ExtensionData", PropertyAttributes.None, _dictionaryStringObjectReference);
propertyDefinition.CustomAttributes.Add(new CustomAttribute(_jsonExtensionDataAttributeReference));
// Add backing field
var field = new FieldDefinition("_extensionData",
FieldAttributes.Private,
_dictionaryStringObjectReference);
field.CustomAttributes.Add(new CustomAttribute(_jsonIgnoreAttributeReference));
typeDefinition.Fields.Add(field);
// Add getter
var get = new MethodDefinition("get_ExtensionData",
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
_dictionaryStringObjectReference);
get.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
get.Body.Instructions.Add(Instruction.Create(OpCodes.Ldfld, field));
get.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
propertyDefinition.GetMethod = get;
typeDefinition.Methods.Add(get);
// Add setter
var set = new MethodDefinition("set_ExtensionData",
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
ModuleDefinition.TypeSystem.Void);
set.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, _dictionaryStringObjectReference));
set.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0));
set.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_1));
set.Body.Instructions.Add(Instruction.Create(OpCodes.Stfld, field));
set.Body.Instructions.Add(Instruction.Create(OpCodes.Ret));
propertyDefinition.SetMethod = set;
typeDefinition.Methods.Add(set);
typeDefinition.Properties.Add(propertyDefinition);
}
}
@@ -1,7 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<JsonExtensionData>
<IncludeNamespacesRegex>SPTarkov\.Server\.Core\.Models.*</IncludeNamespacesRegex>
</JsonExtensionData>
<Virtuosity/>
</Weavers>
@@ -33,7 +33,6 @@
<PackageReference Include="Virtuosity.Fody" Version="3.1.2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<WeaverFiles Include="$(SolutionDir)Libraries\FodyWeavers\JsonExtensionData.Fody\bin\$(Configuration)\netstandard2.0\JsonExtensionData.Fody.dll"/>
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,144 @@
using System.Text.RegularExpressions;
namespace JsonExtensionDataGenerator;
public class JsonExtensionDataGeneratorLauncher
{
private static readonly Regex _recordAndClassRegex = new("^(public record |public class )", RegexOptions.Multiline);
private static readonly Regex _endRecordClassRegex = new("^}", RegexOptions.Multiline);
private static readonly Regex _startRecordClassRegex = new("^{", RegexOptions.Multiline);
private const int StartRecordClassOffset = 3;
private static readonly Regex _extensionFinding =
new(
// https://regexr.com/8f5gf
"^(public){0,1} (record|class) (\\w+(<(\\w+(,){0,1})+>){0,1})(\\(.*\\)){0,1}[\r\n ]*:[\r\n ]*(\\w+(<(\\w+(,){0,1})+>){0,1}([\r\n ]*,[\r\n ]*)*)+"
, RegexOptions.Multiline);
private static readonly Regex _extensionCleanup = new(",.*");
private const string Insertion =
" [JsonExtensionData]\r\n public Dictionary<string, object> ExtensionData { get; set; }\r\n\r\n";
private const string Using = "using System.Text.Json.Serialization;\r\n";
public static void Main(string[] args)
{
var modelFiles = LoadModelFiles();
foreach (var modelFile in modelFiles)
{
ProcessFile(modelFile);
}
}
private static void ProcessFile(string modelFile)
{
Console.WriteLine($"Processing file: {modelFile}...");
var fileName = Path.GetFileName(modelFile);
var content = File.ReadAllText(modelFile);
if (!content.Contains("public record ") && !content.Contains("public class "))
{
Console.WriteLine($"File {fileName} doesnt contain any records or classes, skipping...");
// probably an enum or interface
return;
}
var classesAndRecordsToProcessCount = _recordAndClassRegex.Matches(content).Count;
Console.WriteLine($"Found {classesAndRecordsToProcessCount} records or classes for {fileName}");
var firstTimeFlag = false;
var currentIndex = 0;
try
{
for (var i = 0; i < classesAndRecordsToProcessCount; i++)
{
var startIndex = FindNextClassStartIndex(content, currentIndex);
var endIndex = FindEndClassIndex(content, startIndex);
currentIndex = endIndex;
// Check if this class already has the tag anywhere
if (content.Substring(startIndex, endIndex - startIndex).Contains("[JsonExtensionData]"))
{
Console.WriteLine($"Class index {i} for {fileName} already contains [JsonExtensionData], skipping class...");
continue;
}
if (TryGetExtensions(content, startIndex, endIndex, out var extensions))
{
if (extensions.Any(e => !e.StartsWith("I")))
{
Console.WriteLine($"Class index {i} for {fileName} extends a parent class, skipping...");
continue;
}
}
// At this point we know for sure that we need to insert the [JsonExtensionData]
if (!firstTimeFlag)
{
if (!content.Contains("using System.Text.Json.Serialization;"))
{
Console.WriteLine($"Class index {i} for {fileName} doesnt contain using for Json.Serialization. Adding.");
// insert the using and adjust the indexes
content = Using + content;
startIndex += Using.Length;
endIndex += Using.Length;
currentIndex = endIndex;
}
firstTimeFlag = true;
}
// We need to add StartRecordClassOffset to offset the EOL
var insertionIndex = _startRecordClassRegex.Match(content, startIndex, endIndex - startIndex).Index +
StartRecordClassOffset;
content = content.Insert(insertionIndex, Insertion);
Console.WriteLine($"Class index {i} for {fileName} processed.");
currentIndex += Insertion.Length;
}
var stream = File.Open(modelFile, FileMode.Open);
stream.SetLength(0);
stream.Close();
File.WriteAllText(modelFile, content);
}
catch (Exception e)
{
Console.WriteLine($"Error caught processing {modelFile} file\n{e}");
}
}
private static bool TryGetExtensions(
string content,
int startIndex,
int endIndex,
out IEnumerable<string> extensions
)
{
extensions = null;
var match = _extensionFinding.Match(content, startIndex, endIndex - startIndex);
if (match.Success)
{
var extensionsGroup = match.Groups[8];
extensions = extensionsGroup.Captures.Select(c => _extensionCleanup.Replace(c.Value, ""));
return true;
}
return false;
}
private static int FindEndClassIndex(string content, int currentIndex)
{
// we do +3 cause thats the length of what we are searching for
return _endRecordClassRegex.Match(content, currentIndex).Index + 3;
}
private static int FindNextClassStartIndex(string content, int currentIndex)
{
return _recordAndClassRegex.Match(content, currentIndex).Index;
}
private static IEnumerable<string> LoadModelFiles()
{
var projectDir = Directory.GetParent("./").Parent.Parent.Parent.Parent.Parent;
var modelsDir = Path.Combine(projectDir.FullName, "Libraries", "SPTarkov.Server.Core", "Models");
return Directory.GetFiles(modelsDir, "*.cs", SearchOption.AllDirectories);
}
}
+6 -12
View File
@@ -30,11 +30,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Be
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Reflection", "Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj", "{9073A593-A2F5-471E-9678-B896A7226FD4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Fody", "Fody", "{61D0AD50-5B86-41E6-8B19-F11944AC3EE4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JsonExtensionData", "JsonExtensionData", "{2938742B-34FA-47F2-A50B-E0470FB1D807}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonExtensionData.Fody", "Libraries\FodyWeavers\JsonExtensionData.Fody\JsonExtensionData.Fody.csproj", "{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonExtensionDataGenerator", "Tools\JsonExtensionDataGenerator\JsonExtensionDataGenerator.csproj", "{6F4670CD-6861-47A8-9A02-2B63AD73A929}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -82,10 +78,10 @@ Global
{9073A593-A2F5-471E-9678-B896A7226FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.Build.0 = Release|Any CPU
{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Release|Any CPU.Build.0 = Release|Any CPU
{6F4670CD-6861-47A8-9A02-2B63AD73A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F4670CD-6861-47A8-9A02-2B63AD73A929}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F4670CD-6861-47A8-9A02-2B63AD73A929}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F4670CD-6861-47A8-9A02-2B63AD73A929}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -98,8 +94,6 @@ Global
{4B973AC0-0C60-4853-9AF7-7CB69127473E} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{C24B1FEB-F8AC-434E-998D-5DA4D1687295} = {587959C2-5AFA-4B77-B327-566610F9A289}
{9073A593-A2F5-471E-9678-B896A7226FD4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{61D0AD50-5B86-41E6-8B19-F11944AC3EE4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{2938742B-34FA-47F2-A50B-E0470FB1D807} = {61D0AD50-5B86-41E6-8B19-F11944AC3EE4}
{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8} = {2938742B-34FA-47F2-A50B-E0470FB1D807}
{6F4670CD-6861-47A8-9A02-2B63AD73A929} = {587959C2-5AFA-4B77-B327-566610F9A289}
EndGlobalSection
EndGlobal