-
Notifications
You must be signed in to change notification settings - Fork 569
IEntitySystem Event Subscription Code Generation #6227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
IEntitySystem Event Subscription Code Generation #6227
Conversation
|
very sick!! |
|
was hoping someone would tackle some variant of this eventually |
Centronias
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes to the solution file were done with Rider. The rest of the project files I manually edited based on existing generators (as I was initially struggling immensely to get the generator to even run). So lemme know if I borked anything there.
| public string GetQualifiedName() | ||
| { | ||
| return Namespace == null ? Name : $"{Namespace}.{Name}"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I also added this because I use it to look up Symbols from the partial type info.
Robust.Shared.EntitySystemSubscriptionsGenerator/Diagnostics.cs
Outdated
Show resolved
Hide resolved
| var annotatedIEntitySystems = Aggregate( | ||
| GetIEntityTypeCandidatesContainingAnnotatedMethods(context, AllSubscriptionMemberAttributeName), | ||
| GetIEntityTypeCandidatesContainingAnnotatedMethods(context, NetworkSubscriptionMemberAttributeName), | ||
| GetIEntityTypeCandidatesContainingAnnotatedMethods(context, LocalSubscriptionMemberAttributeName), | ||
| GetIEntityTypeCandidatesContainingAnnotatedMethods(context, CallAfterSubscriptionsAttributeName) | ||
| ) // Get all candidate types containing subscription annotated methods | ||
| .SelectMany((array, _) => array.ToImmutableHashSet(PartialTypeInfo.WithoutLocationEqualityComparer)) // Dedupe | ||
| .Combine(context.CompilationProvider) | ||
| .Select((inputs, cancel) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is admittedly kinda hairy, but the goal was to make it so that we find systems with annotated methods in them without a need for annotating the system itself.
So this discovers methods, walks up to their systems, dedupes those systems, and then processes the systems.
Intermediate values are equatable PartialTypeInfos, so it should be decent for performance. We do look up compilation-dependent information using info from the PartialTypeInfos, but, again, that's to make the usage more streamlined.
| [Generator(LanguageNames.CSharp)] | ||
| public class EntitySystemSubscriptionGenerator : IIncrementalGenerator | ||
| { | ||
| public void Initialize(IncrementalGeneratorInitializationContext context) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is probably too long, but trying to break out parts of it into functions means passing around IncrementalValueProvider<T>s and tons of contexts which all make the function signatures less than legible.
| if (compilation.GetTypeByMetadataName(partialTypeInfo.GetQualifiedName()) is not | ||
| { } entitySystemType) | ||
| return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically anywhere along the way that something goes bad, we return null and just ignore the value, expecting the other analyzer to complain about it for us.
| private sealed record IEntitySystemInfo( | ||
| PartialTypeInfo Type, | ||
| EquatableArray<SubscriptionInfo> Subscriptions, | ||
| EquatableArray<PostSubscriptionInfo> PostSubscriptions | ||
| ); | ||
|
|
||
| private sealed record SubscriptionInfo(string MethodName, SubscriptionType Type, EquatableArray<string> TypeArgs); | ||
|
|
||
| private sealed record PostSubscriptionInfo(string MethodName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the intermediate values between analyzer steps. I think they should be good for incremental generation, but I am very open to being corrected.
|
main issue i can see is this implementation seems to make it so that any system which uses the annotations for event handler generation must then move their other initialization logic into a separate function, this seems unnecessarily clunky and unintuitive imo, my preferred solution at least would be that this does present the issue of people potentially overriding this function unnecessarily or misunderstanding its purpose, and i -thought- rider had easy support for excluding methods from intellisense using |
|
Yeah, I agree that needing to retrofit existing |
PJB3005
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't review the main body of code yet.
Robust.Shared.EntitySystemSubscriptionsGenerator/Diagnostics.cs
Outdated
Show resolved
Hide resolved
....Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs
Outdated
Show resolved
Hide resolved
# Conflicts: # RobustToolbox.sln
- DiagnosticAnalyzer instead of generator for errors - Move diagnostic IDs - Move generated subscriptions to new method, revert stuff around changing `Initialize` - Change everything to expect annotations in `EntitySystem` extenders, not `IEntitySystem` implementors - Document more, specifically detailed doc on the attributes because that's where people will be looking when they say "what the fuck is this annotation?"
Adds an
IIncrementalGeneratorwhich takes annotated event handler methods and assembles them into subscription calls for you inEntitySystems so you don't need to put them inoverride void Initialize().Why?
Most of my experience recently has been in languages with robust type inference, so when I encountered the need to declare and register event handlers with a bunch of type arguments, I got unhappy. For example, consider the client
AudioSystemwhich contains the event handler. To use this handler, we must register it with
. Conceptually, we are just attaching the handle to the method,
OnAudioStartup, to a "Local subscription" concept -- this is the minimum information needed. However, thanks to exactly how subscriptions are registered, and exactly how C# does type parameters, it is necessary to re-specify<AudioComponent, ComponentStartup>on the registration, which doesn't provide much value for coders actually reading and maintaining the code.Additionally, applications built on RobustToolbox are encouraged to declare
EntitySystems aspartial, which can cause subscriptions to need to be split between files, leading to a pattern of the main file having theoverride void Initialize() { ... }implementation which delegates to some number ofvoid InitializeSubsystem() { ... }methods.This generator solves both of these issues by allowing coders to specify "this is an even subscription handler" on the handler method itself, and the generator does the rest, assembling all subscriptions in the class, regardless of partial class parts in disparate files, in one
override AutoSubscriptions() { ... }method.How exactly do I use this?
Check out the example below for something more concrete, but the concept is to simply annotate your event handler where it's declared:
The generator looks at the subscription type (
LocalEventSubscription,NetworkEventSubscription,EventSubscription("all")) and the parameters to the handler method (AudioComponentandComponentStartuphere), to decide what to do. It constructs a method binding for each annotation (here), and then assembles all of those bindings into one
override void AutoSubscriptions()implementation which is put into a generatedpartial classfile.No more jumping between subscription registrations and handlers, do your coding all in one location in the syntax. No more juggling initialization when dealing with partial classes.
What this DOESN'T do
This doesn't support
beforeandafterparameters to event subscription methods. I wanted to, but encoding that information into the annotation proved too annoying.Technical Details
This is implemented with:
LocalEventSubscriptionAttributeNetworkEventSubscriptionAttributeEventSubscriptionAttributeIIncrementalGenerator,EntitySystemSubscriptionGeneratorDiagnosticAnalyzer,EntitySystemSubscriptionGeneratorErrorAnalyzerModifications to Existing Code
IEntitySystemgets a new method,void AutoSubscriptions(), to hold onto the generated code. It's called just afterInitializeis. Ideally this would be declared onEntitySystemsince the actual subscription methods are only available to that type, but that'd require much more change toInitializeand how it's called.IEqualityComparer<PartialTypeInfo>which doesn't consider thePartialTypeInfo's syntaxLocation. This was needed because of exactly how I'm findingIEntitySystems which contain annotated methods, which requires the generator to dedupe types. However, becausePartialTypeInfos constructed from differentpartialparts of the same class are not equal (with the record's implicit implementation), the deduping wouldn't work without explicitly excluding theLocation.AudioSystemExample
Check out e96121b , where I've modified the existing client
AudioSystemto use subscription generation. It generates the following syntax: