I need to design an AST-like class hierarchy for the introspection of code (like the Java Element API used during annotation processing). But I'm unsure how to make it maintainable and easy to use.
My instinct is to model the language as closely as possible and take full advantage of the type system. In order to do this, I would use inheritance to model the intuitive "is a"-relationship between syntax elements to get exhaustion checks in
TypeElements and so on. This leads to quite a deep class hierarchy.
However, shared behaviour doesn't respect this hierarchy. For example:
InterfaceElements can enclose other elements. But
PackageElements only ever enclose other
TypeElements and only
ClassElements ever enclose
ConstructorElements. Mixin-interfaces in combination with delegates for the implementation can be used to share behaviour between the classes. Still, it gets complex very quickly with deep class hierarchies and many mixin interfaces.
Should there be a single interface for elements that are able to enclose other elements? Or an interface for being able to enclose packages, another interface for being able to enclose types, another one for enclosing functions etc. Should there be separate properties for visibility and modality or simply one that contains all applicable modifiers?
The opposite approach (the one chosen by the Java Element API) is to make the hierarchy flat with less specific interfaces: All classes implement the
getEnclosedElements: List<Element> function, with those elements that don't enclose anything simply returning an empty list. Similarly there aren't different types for classes and interfaces. They are just grouped together under
TypeElement with an enum property that specifies which kind it really is. Of course that makes everything much simpler for me, but it's not obvious to the user from the interface alone which classes may enclose which kinds of other elements, so users have to read API documentation much more often and their code is less safe.
Which approach is preferable here? I'm afraid that the deep class hierarchies will make the API undiscoverable, hard to maintain (because so many mixins are necessary to share behaviour between similar classes) and annoying to use. For example: The user may want to write a function to print all the members of a
InterfacElement. They can not use the
EnclosesElements interface because a
PackageElement also encloses elements and also can not use the
TypeElement base class because that includes
TypeAliasElement which never encloses members. There is no interface or base class that includes just