forked from space-wizards/RobustToolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto-componentstate source generator (space-wizards#3684)
* dog what am i doing * finish gen source part from class symbol * we are dangerously close to things happening * generation fixes * oh? on god? * stop autogenerating the attribute for no reason + diagnostics * testing diagnostics * proper type name handling + clonedata bool * thank you material storage for making me realize this * forgot to commit * p * fixes for afterautohandlestate * make it work with access
- Loading branch information
1 parent
b31876f
commit 85547a9
Showing
9 changed files
with
420 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0"> | ||
<ItemGroup> | ||
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Shared.CompNetworkGenerator\Robust.Shared.CompNetworkGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
295 changes: 295 additions & 0 deletions
295
Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
namespace Robust.Shared.CompNetworkGenerator | ||
{ | ||
[Generator] | ||
public class ComponentNetworkGenerator : ISourceGenerator | ||
{ | ||
private const string ClassAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute"; | ||
private const string MemberAttributeName = "Robust.Shared.Analyzers.AutoNetworkedFieldAttribute"; | ||
|
||
private static string GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle) | ||
{ | ||
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString(); | ||
var componentName = classSymbol.Name; | ||
var stateName = $"{componentName}_AutoState"; | ||
|
||
var members = classSymbol.GetMembers(); | ||
var fields = new List<(ITypeSymbol Type, string FieldName, AttributeData Attribute)>(); | ||
var fieldAttr = comp.GetTypeByMetadataName(MemberAttributeName); | ||
|
||
foreach (var mem in members) | ||
{ | ||
var attribute = mem.GetAttributes().FirstOrDefault(a => | ||
a.AttributeClass != null && | ||
a.AttributeClass.Equals(fieldAttr, SymbolEqualityComparer.Default)); | ||
|
||
if (attribute == null) | ||
{ | ||
continue; | ||
} | ||
|
||
switch (mem) | ||
{ | ||
case IFieldSymbol field: | ||
fields.Add((field.Type, field.Name, attribute)); | ||
break; | ||
case IPropertySymbol prop: | ||
{ | ||
if (prop.SetMethod == null || prop.SetMethod.DeclaredAccessibility != Accessibility.Public) | ||
{ | ||
var msg = "Property is marked with [AutoNetworkField], but has no accessible setter method."; | ||
context.ReportDiagnostic( | ||
Diagnostic.Create( | ||
new DiagnosticDescriptor( | ||
"RXN0008", | ||
msg, | ||
msg, | ||
"Usage", | ||
DiagnosticSeverity.Error, | ||
true), | ||
classSymbol.Locations[0])); | ||
continue; | ||
} | ||
|
||
if (prop.GetMethod == null || prop.GetMethod.DeclaredAccessibility != Accessibility.Public) | ||
{ | ||
var msg = "Property is marked with [AutoNetworkField], but has no accessible getter method."; | ||
context.ReportDiagnostic( | ||
Diagnostic.Create( | ||
new DiagnosticDescriptor( | ||
"RXN0008", | ||
msg, | ||
msg, | ||
"Usage", | ||
DiagnosticSeverity.Error, | ||
true), | ||
classSymbol.Locations[0])); | ||
continue; | ||
} | ||
|
||
fields.Add((prop.Type, prop.Name, attribute)); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
if (fields.Count == 0) | ||
{ | ||
var msg = "Component is marked with [AutoGenerateComponentState], but has no valid members marked with [AutoNetworkField]."; | ||
context.ReportDiagnostic( | ||
Diagnostic.Create( | ||
new DiagnosticDescriptor( | ||
"RXN0007", | ||
msg, | ||
msg, | ||
"Usage", | ||
DiagnosticSeverity.Error, | ||
true), | ||
classSymbol.Locations[0])); | ||
|
||
return null; | ||
} | ||
|
||
// eg: | ||
// public string Name = default!; | ||
// public int Count = default!; | ||
var stateFields = new StringBuilder(); | ||
|
||
// eg: | ||
// Name = component.Name, | ||
// Count = component.Count, | ||
var getStateInit = new StringBuilder(); | ||
|
||
// eg: | ||
// component.Name = state.Name; | ||
// component.Count = state.Count; | ||
var handleStateSetters = new StringBuilder(); | ||
|
||
foreach (var (type, name, attribute) in fields) | ||
{ | ||
stateFields.Append($@" | ||
public {type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {name} = default!;"); | ||
|
||
// get first ctor arg of the field attribute, which determines whether the field should be cloned | ||
// (like if its a dict or list) | ||
if (attribute.ConstructorArguments[0].Value is bool val && val) | ||
{ | ||
getStateInit.Append($@" | ||
{name} = component.{name},"); | ||
|
||
handleStateSetters.Append($@" | ||
if (state.{name} != null) | ||
component.{name} = new(state.{name});"); | ||
} | ||
else | ||
{ | ||
getStateInit.Append($@" | ||
{name} = component.{name},"); | ||
|
||
handleStateSetters.Append($@" | ||
component.{name} = state.{name};"); | ||
} | ||
} | ||
|
||
var eventRaise = ""; | ||
if (raiseAfterAutoHandle) | ||
{ | ||
eventRaise = @" | ||
var ev = new AfterAutoHandleStateEvent(args.Current); | ||
EntityManager.EventBus.RaiseComponentEvent(component, ref ev);"; | ||
} | ||
|
||
return $@"// <auto-generated /> | ||
using Robust.Shared.GameStates; | ||
using Robust.Shared.GameObjects; | ||
using Robust.Shared.Analyzers; | ||
using Robust.Shared.Serialization; | ||
namespace {nameSpace}; | ||
public partial class {componentName} | ||
{{ | ||
[Serializable, NetSerializable] | ||
public class {stateName} : ComponentState | ||
{{{stateFields} | ||
}} | ||
[RobustAutoGenerated] | ||
public class {componentName}_AutoNetworkSystem : EntitySystem | ||
{{ | ||
public override void Initialize() | ||
{{ | ||
SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState); | ||
SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState); | ||
}} | ||
private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args) | ||
{{ | ||
args.State = new {stateName} | ||
{{{getStateInit} | ||
}}; | ||
}} | ||
private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args) | ||
{{ | ||
if (args.Current is not {stateName} state) | ||
return; | ||
{handleStateSetters}{eventRaise} | ||
}} | ||
}} | ||
}} | ||
"; | ||
} | ||
|
||
public void Execute(GeneratorExecutionContext context) | ||
{ | ||
var comp = (CSharpCompilation) context.Compilation; | ||
|
||
if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver)) | ||
{ | ||
return; | ||
} | ||
|
||
var symbols = GetAnnotatedTypes(context, comp, receiver); | ||
|
||
// Generate component sources and add | ||
foreach (var type in symbols) | ||
{ | ||
try | ||
{ | ||
var attr = type.Attribute; | ||
var raiseEv = false; | ||
if (attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value != null) | ||
{ | ||
// Get the afterautohandle bool, which is first constructor arg | ||
raiseEv = (bool) attr.ConstructorArguments[0].Value; | ||
} | ||
|
||
var source = GenerateSource(context, type.Type, comp, raiseEv); | ||
// can be null if no members marked with network field, which already has a diagnostic, so | ||
// just continue | ||
if (source == null) | ||
continue; | ||
|
||
context.AddSource($"{type.Type.Name}_CompNetwork.g.cs", SourceText.From(source, Encoding.UTF8)); | ||
} | ||
catch (Exception e) | ||
{ | ||
context.ReportDiagnostic( | ||
Diagnostic.Create( | ||
new DiagnosticDescriptor( | ||
"RXN0003", | ||
"Unhandled exception occured while generating automatic component state handling.", | ||
$"Unhandled exception occured while generating automatic component state handling: {e}", | ||
"Usage", | ||
DiagnosticSeverity.Error, | ||
true), | ||
type.Type.Locations[0])); | ||
} | ||
} | ||
} | ||
|
||
private IReadOnlyList<(INamedTypeSymbol Type, AttributeData Attribute)> GetAnnotatedTypes(in GeneratorExecutionContext context, | ||
CSharpCompilation comp, NameReferenceSyntaxReceiver receiver) | ||
{ | ||
var symbols = new List<(INamedTypeSymbol, AttributeData)>(); | ||
var attributeSymbol = comp.GetTypeByMetadataName(ClassAttributeName); | ||
foreach (var candidateClass in receiver.CandidateClasses) | ||
{ | ||
var model = comp.GetSemanticModel(candidateClass.SyntaxTree); | ||
var typeSymbol = model.GetDeclaredSymbol(candidateClass); | ||
var relevantAttribute = typeSymbol?.GetAttributes().FirstOrDefault(attr => | ||
attr.AttributeClass != null && | ||
attr.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); | ||
|
||
if (relevantAttribute == null) | ||
{ | ||
continue; | ||
} | ||
|
||
var isPartial = candidateClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); | ||
|
||
if (isPartial) | ||
{ | ||
symbols.Add((typeSymbol, relevantAttribute)); | ||
} | ||
else | ||
{ | ||
var missingPartialKeywordMessage = | ||
$"The type {typeSymbol.Name} should be declared with the 'partial' keyword " + | ||
"as it is annotated with the [AutoGenerateComponentState] attribute."; | ||
|
||
context.ReportDiagnostic( | ||
Diagnostic.Create( | ||
new DiagnosticDescriptor( | ||
"RXN0006", | ||
missingPartialKeywordMessage, | ||
missingPartialKeywordMessage, | ||
"Usage", | ||
DiagnosticSeverity.Error, | ||
true), | ||
Location.None)); | ||
} | ||
} | ||
|
||
return symbols; | ||
} | ||
|
||
public void Initialize(GeneratorInitializationContext context) | ||
{ | ||
if (!Debugger.IsAttached) | ||
{ | ||
//Debugger.Launch(); | ||
} | ||
context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver()); | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
Robust.Shared.CompNetworkGenerator/NameReferenceSyntaxReceiver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
||
namespace Robust.Shared.CompNetworkGenerator | ||
{ | ||
public sealed class NameReferenceSyntaxReceiver : ISyntaxReceiver | ||
{ | ||
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>(); | ||
|
||
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) | ||
{ | ||
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && | ||
classDeclarationSyntax.AttributeLists.Count > 0) | ||
CandidateClasses.Add(classDeclarationSyntax); | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" /> | ||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> | ||
</ItemGroup> | ||
</Project> |
Oops, something went wrong.