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 switch
-expressions. I.e. MethodElement
s and ConstructorElement
s are ExecutableElement
s, TypeAliasElement
s, ClassElement
s and InterfaceElement
s are TypeElement
s and so on. This leads to quite a deep class hierarchy.
However, shared behaviour doesn't respect this hierarchy. For example: PackageElement
s, ClassElement
s and InterfaceElement
s can enclose other elements. But PackageElement
s only ever enclose other PackageElement
s or TypeElement
s and only ClassElement
s ever enclose ConstructorElement
s. 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 ClassElement
or 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 InterfaceElement
and ClassElement
.