I naively assumed AST::Type::getAs<T>() would work regardless whether T represents sugar or not, and looking closer it is indeed specialized for TypedefType, TemplateSpecializationType, and AttributeType to make those work, but not for other sugar like SubstTemplateTypeParmType.

For example, t->getAs<SubstTemplateTypeParmType>() does not work if t is a wrapping TypedefType (but happens to work if t itself already is a SubsTemplateTypeParmType). Is that intended?

I don't think it was intended. I also thought I understood how type sugar
and getAs work, and this is a surprise to me as well.

I would've expected getAs to work like this:

template <typename T> const T *Type::getAs() const {
// If this is directly a T type, return it.
if (const T *Ty = dyn_cast<T>(this))
return Ty;
// If the canonical type is directly a T type, return it.
if (const T *Ty = dyn_cast<T>(CanonicalType))
return Ty;
// Do repeated single step desugarings until we find the type or return
null.
...
}

I naively assumed AST::Type::getAs<T>() would work regardless whether T
represents sugar or not, and looking closer it is indeed specialized for
TypedefType, TemplateSpecializationType, and AttributeType to make those
work, but not for other sugar like SubstTemplateTypeParmType.

For example, t->getAs<SubstTemplateTypeParmType>() does not work if t is
a wrapping TypedefType (but happens to work if t itself already is a
SubsTemplateTypeParmType). Is that intended?

I don't think it was intended. I also thought I understood how type sugar
and getAs work, and this is a surprise to me as well.

I would've expected getAs to work like this:

template <typename T> const T *Type::getAs() const {
// If this is directly a T type, return it.
if (const T *Ty = dyn_cast<T>(this))
return Ty;
// If the canonical type is directly a T type, return it.
if (const T *Ty = dyn_cast<T>(CanonicalType))

Some nodes are only sometimes sugar; we should find the outermost one in
such a case and this may find the innermost one.

return Ty;
// Do repeated single step desugarings until we find the type or return
null.
...
}

I too find the current inconsistency troubling, but I don't think the right
course of action is completely clear yet. If I have

template<typename T> struct X { typedef T type; };

and I take the type X<int>::type and getAs<SubstTemplateTypeParmType>(),
should that really find something? (And should getAs<TypedefType>() really
find something?) It would be worth looking through the existing uses to see
what sugar they expected to look through before we apply this to all the
other sugar nodes. I expect there's a useful and consistent answer to be
found here, and perhaps it is that we should always act as if we perform a
single-step desugaring.