Better C++ Syntax Highlighting - Part 2: Enums

· Updated Jul 14, 2025

Enums are a great starting point as their declarations are simple and their usage easy to follow. Consider the following example:

cpp
1
enum class Level {
2
Debug = 0,
3
Info,
4
Warning,
5
Error,
6
Fatal = Error,
7
};
8
9
void log_message(Level level, const char* message);
10
11
int main() {
12
log_message(Level::Error, "something bad happened");
13
// ...
14
}

The AST for this snippet looks like this:

text
|-EnumDecl 0x1b640a490d8 <.\example.cpp:1:1, line:7:1> line:1:12 referenced class Level 'int'
| |-EnumConstantDecl 0x1b640a491f8 <line:2:5, col:13> col:5 Debug 'Level'
| | `-ConstantExpr 0x1b640a491d0 <col:13> 'int'
| | |-value: Int 0
| | `-IntegerLiteral 0x1b640a491b0 <col:13> 'int' 0
| |-EnumConstantDecl 0x1b642245648 <line:3:5> col:5 Info 'Level'
| |-EnumConstantDecl 0x1b6422456a8 <line:4:5> col:5 Warning 'Level'
| |-EnumConstantDecl 0x1b642245708 <line:5:5> col:5 referenced Error 'Level'
| `-EnumConstantDecl 0x1b6422457a8 <line:6:5, col:13> col:5 Fatal 'Level'
| `-ConstantExpr 0x1b642245780 <col:13> 'int'
| |-value: Int 3
| `-DeclRefExpr 0x1b642245760 <col:13> 'int' EnumConstant 0x1b642245708 'Error' 'Level'
|-FunctionDecl 0x1b642245a28 <line:9:1, col:50> col:6 used log_message 'void (Level, const char *)'
| |-ParmVarDecl 0x1b642245848 <col:18, col:24> col:24 level 'Level'
| `-ParmVarDecl 0x1b6422458d0 <col:31, col:43> col:43 message 'const char *'
`-FunctionDecl 0x1b642245bb0 <line:11:1, line:14:1> line:11:5 main 'int ()'
`-CompoundStmt 0x1b642245ee0 <col:12, line:14:1>
`-CallExpr 0x1b642245e98 <line:12:5, col:55> 'void'
|-ImplicitCastExpr 0x1b642245e80 <col:5> 'void (*)(Level, const char *)' <FunctionToPointerDecay>
| `-DeclRefExpr 0x1b642245e00 <col:5> 'void (Level, const char *)' lvalue Function 0x1b642245a28 'log_message' 'void (Level, const char *)'
|-DeclRefExpr 0x1b642245d40 <col:17, col:24> 'Level' EnumConstant 0x1b642245708 'Error' 'Level'
| `-NestedNameSpecifier TypeSpec 'Level'
`-ImplicitCastExpr 0x1b642245ec8 <col:31> 'const char *' <ArrayToPointerDecay>
`-StringLiteral 0x1b642245dd0 <col:31> 'const char[23]' lvalue "something bad happened"

Enum Declarations

Enums are represented by two node types in the AST: an EnumDecl node for the declaration itself, and an EnumConstantDecl node for each enum constant. From the EnumDecl node above, we can infer that Level is declared as an enum class, and that it’s underlying type is an int. If we had explicitly set this to a type like unsigned char or std::uint8_t, this would be also reflected in the AST.

We’ll set up visitors for both EnumDecl and EnumConstantDecl nodes:

cpp
bool VisitEnumDecl(clang::EnumDecl* node);
bool VisitEnumConstantDecl(clang::EnumConstantDecl* node);

The implementation of VisitEnumDecl looks like this:

cpp
1
bool Visitor::VisitEnumDecl(clang::EnumDecl* node) {
2
const clang::SourceManager& source_manager = m_context->getSourceManager();
3
const clang::SourceLocation& source_location = node->getLocation();
4
5
// Skip any enum definitions that do not come from the main file
6
if (!source_manager.isInMainFile(source_location)) {
7
return true;
8
}
9
10
const std::string& name = node->getNameAsString();
11
unsigned line = source_manager.getSpellingLineNumber(source_location);
12
unsigned column = source_manager.getSpellingColumnNumber(source_location);
13
14
m_annotator->insert_annotation("enum-name", line, column, name.length());
15
return true;
16
}

This inserts an enum-name annotation for every enum declaration.

The return value of a visitor function indicates whether we want AST traversal to continue. Since we are interested in traversing all the nodes of the AST, this will always be true.

As mentioned in the previous post in this series, the SourceManager class maps AST nodes back to their source locations within the translation unit. The isInMainFile() check ensures that the node originates from the “main” file we are annotating - the one provided to runToolOnCodeWithArgs. This prevents annotations from being applied to external headers, and is a recurring pattern in every visitor we will implement.

The visitor for EnumConstantDecl nodes is nearly identical, except that it inserts an enum-value annotation instead of enum-name:

cpp
1
bool Visitor::VisitEnumConstantDecl(clang::EnumConstantDecl* node) {
2
// Check to ensure this node originates from the file we are annotating
3
// ...
4
5
const std::string& name = node->getNameAsString();
6
unsigned line = source_manager.getSpellingLineNumber(source_location);
7
unsigned column = source_manager.getSpellingColumnNumber(source_location);
8
9
m_annotator->insert_annotation("enum-value", line, column, name.length());
10
return true;
11
}

With both visitors implemented, our tool produces the following output:

text
1
enum class [[enum-name,Level]] {
2
[[enum-value,Debug]] = 0,
3
[[enum-value,Info]],
4
[[enum-value,Warning]],
5
[[enum-value,Error]],
6
[[enum-value,Fatal]] = Error,
7
};
8
9
void log_message(Level level, const char* message);
10
11
int main() {
12
log_message(Level::Error, "something bad happened");
13
// ...
14
}

This is a good start, but it’s not yet complete. The references to Error on line 6 and 12 are not declarations, so we’ll need a new visitor to annotate these.

Enum References

References to enum values are captured by DeclRefExpr nodes, which represent expressions that refer to previously declared variables, functions, and types. This is confirmed by line 21 of the AST, which represents the Level::Error reference in main():

text
DeclRefExpr 0x1b642245d40 <col:17, col:24> 'Level' EnumConstant 0x1b642245708 'Error' 'Level'

We’ll add a new visitor for DeclRefExpr nodes:

cpp
bool Visitor::VisitDeclRefExpr(clang::DeclRefExpr* node);

The implementation of VisitDeclRefExpr is very similar to the VisitEnumDecl and VisitEnumConstantDecl visitor functions:

cpp
1
bool Visitor::VisitDeclRefExpr(clang::DeclRefExpr* node) {
2
// Check to ensure this node originates from the file we are annotating
3
// ...
4
5
const clang::SourceLocation& location = node->getLocation();
6
unsigned line = source_manager.getSpellingLineNumber(location);
7
unsigned column = source_manager.getSpellingColumnNumber(location);
8
9
if (clang::ValueDecl* decl = node->getDecl()) {
10
const std::string& name = decl->getNameAsString();
11
12
if (const clang::EnumConstantDecl* ec = clang::dyn_cast<clang::EnumConstantDecl>(decl)) {
13
m_annotator->insert_annotation("enum-value", line, column, name.length());
14
}
15
16
visit_qualifiers(decl->getDeclContext(), node->getSourceRange());
17
}
18
19
return true;
20
}

We use getDecl() to retrieve information about the underlying declaration being referenced. If it’s an EnumConstantDecl, we insert an enum-value annotation for the node.

With this visitor implemented, we are able to annotate references to enum constants as well:

text
 
enum class Level {
 
Debug = 0,
 
Info,
 
Warning,
 
Error,
+
Fatal = [[enum-value,Error]],
 
};
 
 
int main() {
+
log_message(Level::[[enum-value,Error]], "something bad happened");
 
// ...
 
}

Styling

The final step is to add definitions for the enum-name and enum-value CSS styles:

css
.language-cpp .enum-name {
color: rgb(181, 182, 227);
}
.language-cpp .enum-value {
color: rgb(199, 125, 187);
}
cpp
enum class Level {
Debug = 0,
Info,
Warning,
Error,
Fatal = Error,
};
void log_message(Level level, const char* message);
int main() {
log_message(Level::Error, "something bad happened");
// ...
}

We’ve configured the first (of many!) visitors to handle enum declarations and references to enum constants, and established some common patterns that we’ll use throughout this series. In the <LocalLink text={“next post”} to={“Better C++ Syntax Highlighting - Part 3: Namespaces”}>, we’ll expand our tool to annotate namespaces. Thanks for reading!

Series: Better C++ Syntax Highlighting - Part 2 of 10