Skip to content

Commit 7b8f7be

Browse files
authored
[clang-tidy] Add new check bugprone-tagged-union-member-count (#89925)
This patch introduces a new check to find mismatches between the number of data members in a union and the number enum values present in variant-like structures. Variant-like types can look something like this: ```c++ struct variant { enum { tag1, tag2, } kind; union { int i; char c; } data; }; ``` The kind data member of the variant is supposed to tell which data member of the union is valid, however if there are fewer enum values than union members, then it is likely a mistake. The opposite is not that obvious, because it might be fine to have more enum values than union data members, but for the time being I am curious how many real bugs can be caught if we give a warning regardless. This patch also contains a heuristic where we try to guess whether the last enum constant is actually supposed to be a tag value for the variant or whether it is just holding how many enum constants have been created. Patch by Gábor Tóthvári!
1 parent cc01112 commit 7b8f7be

19 files changed

+1859
-0
lines changed

clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
#include "SuspiciousStringviewDataUsageCheck.h"
7979
#include "SwappedArgumentsCheck.h"
8080
#include "SwitchMissingDefaultCaseCheck.h"
81+
#include "TaggedUnionMemberCountCheck.h"
8182
#include "TerminatingContinueCheck.h"
8283
#include "ThrowKeywordMissingCheck.h"
8384
#include "TooSmallLoopVariableCheck.h"
@@ -229,6 +230,8 @@ class BugproneModule : public ClangTidyModule {
229230
"bugprone-suspicious-stringview-data-usage");
230231
CheckFactories.registerCheck<SwappedArgumentsCheck>(
231232
"bugprone-swapped-arguments");
233+
CheckFactories.registerCheck<TaggedUnionMemberCountCheck>(
234+
"bugprone-tagged-union-member-count");
232235
CheckFactories.registerCheck<TerminatingContinueCheck>(
233236
"bugprone-terminating-continue");
234237
CheckFactories.registerCheck<ThrowKeywordMissingCheck>(

clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ add_clang_library(clangTidyBugproneModule
7373
SuspiciousSemicolonCheck.cpp
7474
SuspiciousStringCompareCheck.cpp
7575
SwappedArgumentsCheck.cpp
76+
TaggedUnionMemberCountCheck.cpp
7677
TerminatingContinueCheck.cpp
7778
ThrowKeywordMissingCheck.cpp
7879
TooSmallLoopVariableCheck.cpp
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//===--- TaggedUnionMemberCountCheck.cpp - clang-tidy ---------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "TaggedUnionMemberCountCheck.h"
10+
#include "../utils/OptionsUtils.h"
11+
#include "clang/ASTMatchers/ASTMatchFinder.h"
12+
#include "llvm/ADT/STLExtras.h"
13+
#include "llvm/ADT/SmallSet.h"
14+
15+
using namespace clang::ast_matchers;
16+
17+
namespace clang::tidy::bugprone {
18+
19+
static constexpr llvm::StringLiteral StrictModeOptionName = "StrictMode";
20+
static constexpr llvm::StringLiteral EnableCountingEnumHeuristicOptionName =
21+
"EnableCountingEnumHeuristic";
22+
static constexpr llvm::StringLiteral CountingEnumPrefixesOptionName =
23+
"CountingEnumPrefixes";
24+
static constexpr llvm::StringLiteral CountingEnumSuffixesOptionName =
25+
"CountingEnumSuffixes";
26+
27+
static constexpr bool StrictModeOptionDefaultValue = false;
28+
static constexpr bool EnableCountingEnumHeuristicOptionDefaultValue = true;
29+
static constexpr llvm::StringLiteral CountingEnumPrefixesOptionDefaultValue =
30+
"";
31+
static constexpr llvm::StringLiteral CountingEnumSuffixesOptionDefaultValue =
32+
"count";
33+
34+
static constexpr llvm::StringLiteral RootMatchBindName = "root";
35+
static constexpr llvm::StringLiteral UnionMatchBindName = "union";
36+
static constexpr llvm::StringLiteral TagMatchBindName = "tags";
37+
38+
namespace {
39+
40+
AST_MATCHER_P2(RecordDecl, fieldCountOfKindIsOne,
41+
ast_matchers::internal::Matcher<FieldDecl>, InnerMatcher,
42+
StringRef, BindName) {
43+
// BoundNodesTreeBuilder resets itself when a match occurs.
44+
// So to avoid losing previously saved binds, a temporary instance
45+
// is used for matching.
46+
//
47+
// For precedence, see commit: 5b07de1a5faf4a22ae6fd982b877c5e7e3a76559
48+
clang::ast_matchers::internal::BoundNodesTreeBuilder TempBuilder;
49+
50+
const FieldDecl *FirstMatch = nullptr;
51+
for (const FieldDecl *Field : Node.fields()) {
52+
if (InnerMatcher.matches(*Field, Finder, &TempBuilder)) {
53+
if (FirstMatch) {
54+
return false;
55+
} else {
56+
FirstMatch = Field;
57+
}
58+
}
59+
}
60+
61+
if (FirstMatch) {
62+
Builder->setBinding(BindName, clang::DynTypedNode::create(*FirstMatch));
63+
return true;
64+
}
65+
return false;
66+
}
67+
68+
} // namespace
69+
70+
TaggedUnionMemberCountCheck::TaggedUnionMemberCountCheck(
71+
StringRef Name, ClangTidyContext *Context)
72+
: ClangTidyCheck(Name, Context),
73+
StrictMode(
74+
Options.get(StrictModeOptionName, StrictModeOptionDefaultValue)),
75+
EnableCountingEnumHeuristic(
76+
Options.get(EnableCountingEnumHeuristicOptionName,
77+
EnableCountingEnumHeuristicOptionDefaultValue)),
78+
CountingEnumPrefixes(utils::options::parseStringList(
79+
Options.get(CountingEnumPrefixesOptionName,
80+
CountingEnumPrefixesOptionDefaultValue))),
81+
CountingEnumSuffixes(utils::options::parseStringList(
82+
Options.get(CountingEnumSuffixesOptionName,
83+
CountingEnumSuffixesOptionDefaultValue))) {
84+
if (!EnableCountingEnumHeuristic) {
85+
if (Options.get(CountingEnumPrefixesOptionName))
86+
configurationDiag("%0: Counting enum heuristic is disabled but "
87+
"%1 is set")
88+
<< Name << CountingEnumPrefixesOptionName;
89+
if (Options.get(CountingEnumSuffixesOptionName))
90+
configurationDiag("%0: Counting enum heuristic is disabled but "
91+
"%1 is set")
92+
<< Name << CountingEnumSuffixesOptionName;
93+
}
94+
}
95+
96+
void TaggedUnionMemberCountCheck::storeOptions(
97+
ClangTidyOptions::OptionMap &Opts) {
98+
Options.store(Opts, StrictModeOptionName, StrictMode);
99+
Options.store(Opts, EnableCountingEnumHeuristicOptionName,
100+
EnableCountingEnumHeuristic);
101+
Options.store(Opts, CountingEnumPrefixesOptionName,
102+
utils::options::serializeStringList(CountingEnumPrefixes));
103+
Options.store(Opts, CountingEnumSuffixesOptionName,
104+
utils::options::serializeStringList(CountingEnumSuffixes));
105+
}
106+
107+
void TaggedUnionMemberCountCheck::registerMatchers(MatchFinder *Finder) {
108+
109+
auto UnionField = fieldDecl(hasType(qualType(
110+
hasCanonicalType(recordType(hasDeclaration(recordDecl(isUnion())))))));
111+
112+
auto EnumField = fieldDecl(hasType(
113+
qualType(hasCanonicalType(enumType(hasDeclaration(enumDecl()))))));
114+
115+
auto hasOneUnionField = fieldCountOfKindIsOne(UnionField, UnionMatchBindName);
116+
auto hasOneEnumField = fieldCountOfKindIsOne(EnumField, TagMatchBindName);
117+
118+
Finder->addMatcher(recordDecl(anyOf(isStruct(), isClass()), hasOneUnionField,
119+
hasOneEnumField, unless(isImplicit()))
120+
.bind(RootMatchBindName),
121+
this);
122+
}
123+
124+
bool TaggedUnionMemberCountCheck::isCountingEnumLikeName(StringRef Name) const {
125+
if (llvm::any_of(CountingEnumPrefixes, [Name](StringRef Prefix) -> bool {
126+
return Name.starts_with_insensitive(Prefix);
127+
}))
128+
return true;
129+
if (llvm::any_of(CountingEnumSuffixes, [Name](StringRef Suffix) -> bool {
130+
return Name.ends_with_insensitive(Suffix);
131+
}))
132+
return true;
133+
return false;
134+
}
135+
136+
std::pair<const std::size_t, const EnumConstantDecl *>
137+
TaggedUnionMemberCountCheck::getNumberOfEnumValues(const EnumDecl *ED) {
138+
llvm::SmallSet<llvm::APSInt, 16> EnumValues;
139+
140+
const EnumConstantDecl *LastEnumConstant = nullptr;
141+
for (const EnumConstantDecl *Enumerator : ED->enumerators()) {
142+
EnumValues.insert(Enumerator->getInitVal());
143+
LastEnumConstant = Enumerator;
144+
}
145+
146+
if (EnableCountingEnumHeuristic && LastEnumConstant &&
147+
isCountingEnumLikeName(LastEnumConstant->getName()) &&
148+
(LastEnumConstant->getInitVal() == (EnumValues.size() - 1))) {
149+
return {EnumValues.size() - 1, LastEnumConstant};
150+
}
151+
152+
return {EnumValues.size(), nullptr};
153+
}
154+
155+
void TaggedUnionMemberCountCheck::check(
156+
const MatchFinder::MatchResult &Result) {
157+
const auto *Root = Result.Nodes.getNodeAs<RecordDecl>(RootMatchBindName);
158+
const auto *UnionField =
159+
Result.Nodes.getNodeAs<FieldDecl>(UnionMatchBindName);
160+
const auto *TagField = Result.Nodes.getNodeAs<FieldDecl>(TagMatchBindName);
161+
162+
assert(Root && "Root is missing!");
163+
assert(UnionField && "UnionField is missing!");
164+
assert(TagField && "TagField is missing!");
165+
if (!Root || !UnionField || !TagField)
166+
return;
167+
168+
const auto *UnionDef =
169+
UnionField->getType().getCanonicalType().getTypePtr()->getAsRecordDecl();
170+
const auto *EnumDef = llvm::dyn_cast<EnumDecl>(
171+
TagField->getType().getCanonicalType().getTypePtr()->getAsTagDecl());
172+
173+
assert(UnionDef && "UnionDef is missing!");
174+
assert(EnumDef && "EnumDef is missing!");
175+
if (!UnionDef || !EnumDef)
176+
return;
177+
178+
const std::size_t UnionMemberCount = llvm::range_size(UnionDef->fields());
179+
auto [TagCount, CountingEnumConstantDecl] = getNumberOfEnumValues(EnumDef);
180+
181+
if (UnionMemberCount > TagCount) {
182+
diag(Root->getLocation(),
183+
"tagged union has more data members (%0) than tags (%1)!")
184+
<< UnionMemberCount << TagCount;
185+
} else if (StrictMode && UnionMemberCount < TagCount) {
186+
diag(Root->getLocation(),
187+
"tagged union has fewer data members (%0) than tags (%1)!")
188+
<< UnionMemberCount << TagCount;
189+
}
190+
191+
if (CountingEnumConstantDecl) {
192+
diag(CountingEnumConstantDecl->getLocation(),
193+
"assuming that this constant is just an auxiliary value and not "
194+
"used for indicating a valid union data member",
195+
DiagnosticIDs::Note);
196+
}
197+
}
198+
199+
} // namespace clang::tidy::bugprone
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//===--- TaggedUnionMemberCountCheck.h - clang-tidy -------------*- C++ -*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_TAGGEDUNIONMEMBERCOUNTCHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_TAGGEDUNIONMEMBERCOUNTCHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
14+
namespace clang::tidy::bugprone {
15+
16+
/// Gives warnings for tagged unions, where the number of tags is
17+
/// different from the number of data members inside the union.
18+
///
19+
/// For the user-facing documentation see:
20+
/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/tagged-union-member-count.html
21+
class TaggedUnionMemberCountCheck : public ClangTidyCheck {
22+
public:
23+
TaggedUnionMemberCountCheck(StringRef Name, ClangTidyContext *Context);
24+
void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
25+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
26+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
27+
28+
private:
29+
const bool StrictMode;
30+
const bool EnableCountingEnumHeuristic;
31+
const std::vector<StringRef> CountingEnumPrefixes;
32+
const std::vector<StringRef> CountingEnumSuffixes;
33+
34+
std::pair<const std::size_t, const EnumConstantDecl *>
35+
getNumberOfEnumValues(const EnumDecl *ED);
36+
bool isCountingEnumLikeName(StringRef Name) const;
37+
};
38+
39+
} // namespace clang::tidy::bugprone
40+
41+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_TAGGEDUNIONMEMBERCOUNTCHECK_H

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ Improvements to clang-tidy
103103
New checks
104104
^^^^^^^^^^
105105

106+
- New :doc:`bugprone-tagged-union-member-count
107+
<clang-tidy/checks/bugprone/tagged-union-member-count>` check.
108+
109+
Gives warnings for tagged unions, where the number of tags is
110+
different from the number of data members inside the union.
111+
106112
New check aliases
107113
^^^^^^^^^^^^^^^^^
108114

0 commit comments

Comments
 (0)