[clang] Redundant elaborated-type keyword leaks into printed type names (aka, PrintingPolicy::SuppressTagKeyword ignored)
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
### Summary
Since the elaborated-type keyword moved from `ElaboratedType` onto the type nodes themselves (#147835 and follow-ups), a *redundant* `typename` / `struct` / `class` / `enum` in the spelling of a type survives into the printed name. Two places are affected:
1. **Diagnostics.** `aka '...'` now reproduces the keyword, so the same type prints differently depending on how the alias that named it was spelled.
2. **`TypePrinter`.** The print sites that emit the keyword do not consult `PrintingPolicy::SuppressTagKeyword`, which defaults to `LangOpts.CPlusPlus` (i.e. on for every C++ compilation) and which API users set explicitly to get a keyword-free name.
This is a regression from clang 21. It is not about dependent names, where `typename` is meaningful — only about spellings where the keyword is redundant.
### Reproducer (no headers)
```c++
namespace NS {
struct Rec {};
enum E { e0 };
template struct Tmpl { T t; };
}
using AliasTmpl = typename NS::Tmpl;
using PlainTmpl = NS::Tmpl;
using AliasRec = struct NS::Rec;
using PlainRec = NS::Rec;
using AliasEnum = enum NS::E;
using PlainEnum = NS::E;
struct Sink { Sink() = delete; };
void f() {
Sink s1 = AliasTmpl();
Sink s2 = PlainTmpl();
Sink s3 = AliasRec();
Sink s4 = PlainRec();
Sink s5 = AliasEnum();
Sink s6 = PlainEnum();
}
```
`Alias*` and `Plain*` name the *same* type in each pair, so the six `aka`s should be identical in pairs.
**clang 21.1.8** — they are:
```
no viable conversion from 'AliasTmpl' (aka 'Tmpl')
no viable conversion from 'PlainTmpl' (aka 'Tmpl')
no viable conversion from 'AliasRec' (aka 'NS::Rec')
no viable conversion from 'PlainRec' (aka 'NS::Rec')
no viable conversion from 'AliasEnum' (aka 'NS::E')
no viable conversion from 'PlainEnum' (aka 'NS::E')
```
**clang 22.1.8** — they are not:
```
no viable conversion from 'AliasTmpl' (aka 'typename NS::Tmpl') <-- 'typename'
no viable conversion from 'PlainTmpl' (aka 'NS::Tmpl')
no viable conversion from 'AliasRec' (aka 'struct NS::Rec') <-- 'struct'
no viable conversion from 'PlainRec' (aka 'NS::Rec')
no viable conversion from 'AliasEnum' (aka 'enum NS::E') <-- 'enum'
no viable conversion from 'PlainEnum' (aka 'NS::E')
```
The point of `aka` is to show what the sugar resolves to; here it reproduces the sugar's spelling instead. It also happens outside any template — the `typename` in `using AliasTmpl = typename NS::Tmpl;` is pure noise.
### Second half: `SuppressTagKeyword` is ignored
The diagnostic above is the visible symptom; API users hit it directly. Printing a type with `PrintingPolicy::SuppressTagKeyword = true` explicitly (libTooling, stock clang 22.1.8, `getSingleStepDesugaredType()` of each field):
| field | desugared node | printed with `SuppressTagKeyword = true` |
|---|---|---|
| `Container::seq_type` | `TemplateSpecializationType` | `typename NS::Vec` |
| `rec_t` | `RecordType` | `struct NS::Rec` |
| `enum_t` | `EnumType` | `enum NS::E` |
`SuppressTagKeyword` defaults to `LO.CPlusPlus`, so this affects every C++
consumer of the printer, not just those who set it by hand. Its documented
purpose is exactly the case that now regressed:
```c++
/// Whether type printing should skip printing the tag keyword.
///
/// This is used when printing the inner type of elaborated types,
/// (as the tag keyword is part of the elaborated type):
///
/// \code
/// struct Geometry::Point;
/// \endcode
unsigned SuppressTagKeyword : 1;
```
After the refactor there is no "inner type" to print any more — the keyword is on the node itself — so at these sites the flag has quietly become a no-op rather than being reimplemented.
### Where it comes from
In #147835, `printTagType()` was split. The keyword print in the new non-canonical branch is unguarded, whereas the code it replaced was not:
```c++
bool PrintedKindDecoration = false;
- if (!Policy.SuppressTagKeyword && !D->getTypedefNameForAnonDecl()) {
- PrintedKindDecoration = true;
- OS << D->getKindName();
- OS << ' ';
+ if (T->isCanonicalUnqualified()) {
+ if (!Policy.SuppressTagKeyword && !D->getTypedefNameForAnonDecl()) {
+ PrintedKindDecoration = true;
+ OS << D->getKindName();
+ OS << ' ';
+ }
+ } else {
+ OS << TypeWithKeyword::getKeywordName(T->getKeyword()); // <-- unguarded
+ ...
}
```
and the deleted `printElaboratedBefore()` did consult the policy:
```c++
- if (!Policy.SuppressTagKeyword && Policy.SuppressScope &&
- !Policy.SuppressUnwrittenScope) {
- bool OldTagKeyword = Policy.SuppressTagKeyword;
- ...
- Policy.SuppressTagKeyword = true;
```
The same applies to every other site that emits the keyword. On current `main` (`clang/lib/AST/TypePrinter.cpp`), none of them checks the policy:
| site | line | checks `SuppressTagKeyword`? |
|---|---|---|
| `printUnresolvedUsingBefore` | 1276 | no |
| `printUsingBefore` | 1293 | no |
| `printTypedefBefore` | 1309 | no |
| `printDeducedTemplateSpecializationBefore` | 1446 | no |
| `printTagType`, non-canonical branch | 1573 | no (the canonical branch at 1567 does) |
| `printTemplateId` | 1758 | no |
| `printDependentNameBefore` | 1809 | no — **and correctly so** |
`printDependentNameBefore` is deliberately excluded: for a dependent name the `typename` is not redundant spelling, it is the only thing saying the name denotes a type.
For the diagnostic half specifically, `desugarForDiagnostic()` in `clang/lib/AST/ASTDiagnostic.cpp` used to drop the keyword together with the wrapper, with an explicit comment saying that is the intent:
```c++
// Don't aka just because we saw an elaborated type...
if (const ElaboratedType *ET = dyn_cast(Ty)) {
QT = ET->desugar();
continue;
}
```
That loop is gone in clang 22 and nothing replaced it; the surrounding code now propagates the keyword instead (e.g. the `TemplateSpecializationType` rebuild passes `TST->getKeyword()` straight through). So the stated intent — *don't aka just because of an elaborated type* — is no longer implemented.
### Suggested fix
Honour `Policy.SuppressTagKeyword` at the print sites listed above (all except `printDependentNameBefore`), and restore the keyword-dropping step in `desugarForDiagnostic()` so that a redundant keyword does not change an `aka`.
### Impact
ROOT/cling reached this through type *normalization*: normalized type names are what the I/O uses to match a data member against its on-file description, and a stray `typename` made members silently not be read back (root-project/root#23055). We work around it downstream, and we already carry a local patch adding the `!Policy.SuppressTagKeyword` guard to `printTagType()`'s non-canonical branch, which is why only `typename` bites us today.
FYI, @vgvassilev
Contributor guide
Research direction
Start with clang/lib/AST/TypePrinter.cpp and clang/lib/AST/ASTDiagnostic.cpp, then run the no-header C++ reproducer from the issue to observe the inconsistent aka output and SuppressTagKeyword behavior. Done means the listed non-dependent print sites honor the policy, desugarForDiagnostic drops redundant elaborated-type keywords, and the dependent-name case remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100