The AST nodes for template code, and RecursiveASTVisitor’s traversal, have inconsistencies that cause bugs in tools. (Example, another and I’ve heard of similar in clang-tidy). I’d like to fix some of these, but want some feedback on what we’re aiming for:
- which nodes should be generated?
- which source locations these nodes correspond to?
- what subset should RAV traverse when
shouldVisitTemplateInstantiations()
is false?
It’d be really useful if by default RAV visited each significant location exactly once. This seems to be the spirit behind !shouldVisitTemplateInstantiations()
- to traverse the code as written. But this is pretty badly broken today in the presence of explicit instantiation.
An easy case is implicit template instantiation. This looks like:
template <typename T> T var = T();
int z = var<int>;
Here there are three interesting declarations:
- a
VarTemplateDecl
- a generic “templated”
VarDecl
forvar<T>
- a specific
VarTemplateSpecializationDecl
forvar<int>
, withgetTemplateSpecializationKind() == TSK_ImplicitInstantiation
Their locations all point at the template, and RAV skips the VarTemplateSpecializationDecl
.
This is as expected, and classes/functions behave the same way.
(There’s no FunctionTemplateSpecializationDecl
, just another FunctionDecl
, but it’s okay).
My mental model is: template instantiation produces some fictitious code: int var<int> = int();
Because this code was produced by instantiation, it is not traversed at all.
A difficult and buggy case is explicit template instantiation. This looks like:
template <typename T> T var = T();
template int var<int>;
Again, this generates three nodes:
- a
VarTemplateDecl
- a generic “templated”
VarDecl
- a
VarTemplateSpecializationDecl
withgetTemplateSpecializationKind() == TSK_ImplicitInstantiation
Locations are inconsistent between template kinds:
-
The
VarTemplateSpecializationDecl
's locations point at the template. This means there is no AST node pointing at the second line of code, and there are multiple traversed AST nodes pointing at the first line. -
Functions have the same behaviour as variables.
-
Classes behave differently: the locations of a
ClassTemplateSpecializationDecl
will point at the explicit specialization! (Obviously not for locations inside the body).
Traversal is also inconsistent:
-
the
VarTemplateSpecializationDecl
is traversed, but not its initializer (which was instantiated, rather than written) -
the
FunctionDecl
is not traversed at all! If explicitly passed toTraverseDecl()
, the instantiated body will also be visited. -
the
ClassTemplateSpecializationDecl
is traversed, but not its body (which was instantiated, rather than written).
My conclusion is that class templates behavior is basically correct here and we should fix functions/variables to match:
- explicit instantiations produce a node whose “body” is instantiated and whose “head” was written.
- The node should be visited and its head should be traversed, but not its body, including when passed to
TraverseDecl()
. - Its head locations should point at the explicit instantiation, its body locations should point at the template.
Do others agree?
If this makes sense, my first step is to document this in RecursiveASTVisitor, and add tests of the current behavior (with FIXMEs). Details: ⚙ D120498 [AST] Test RecursiveASTVisitor's current traversal of templates.
That patch uncovers a couple of other discrepancies (e.g. initializers of VarTemplateSpecializationDecl being skipped too often). I think these are fairly clear-cut bugs where the right thing to do is more obvious, but feel free to comment if I got something wrong.