Skip to content
Merged
3 changes: 3 additions & 0 deletions clang-tools-extra/clang-tidy/bugprone/BugproneTidyModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "IncorrectRoundingsCheck.h"
#include "InfiniteLoopCheck.h"
#include "IntegerDivisionCheck.h"
#include "InvalidEnumDefaultInitializationCheck.h"
#include "LambdaFunctionNameCheck.h"
#include "MacroParenthesesCheck.h"
#include "MacroRepeatedSideEffectsCheck.h"
Expand Down Expand Up @@ -165,6 +166,8 @@ class BugproneModule : public ClangTidyModule {
CheckFactories.registerCheck<InfiniteLoopCheck>("bugprone-infinite-loop");
CheckFactories.registerCheck<IntegerDivisionCheck>(
"bugprone-integer-division");
CheckFactories.registerCheck<InvalidEnumDefaultInitializationCheck>(
"bugprone-invalid-enum-default-initialization");
CheckFactories.registerCheck<LambdaFunctionNameCheck>(
"bugprone-lambda-function-name");
CheckFactories.registerCheck<MacroParenthesesCheck>(
Expand Down
1 change: 1 addition & 0 deletions clang-tools-extra/clang-tidy/bugprone/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_clang_library(clangTidyBugproneModule STATIC
InaccurateEraseCheck.cpp
IncorrectEnableIfCheck.cpp
IncorrectEnableSharedFromThisCheck.cpp
InvalidEnumDefaultInitializationCheck.cpp
UnintendedCharOstreamOutputCheck.cpp
ReturnConstRefFromParameterCheck.cpp
SuspiciousStringviewDataUsageCheck.cpp
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//===--- InvalidEnumDefaultInitializationCheck.cpp - clang-tidy -----------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "InvalidEnumDefaultInitializationCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/TypeVisitor.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include <algorithm>

using namespace clang::ast_matchers;

namespace clang::tidy::bugprone {

namespace {

bool isCompleteAndHasNoZeroValue(const EnumDecl *D) {
const EnumDecl *Definition = D->getDefinition();
return Definition && Definition->isComplete() &&
!Definition->enumerators().empty() &&
std::none_of(Definition->enumerator_begin(),
Definition->enumerator_end(),
[](const EnumConstantDecl *Value) {
return Value->getInitVal().isZero();
});
}

AST_MATCHER(EnumDecl, isCompleteAndHasNoZeroValue) {
return isCompleteAndHasNoZeroValue(&Node);
}

// Find an initialization which initializes the value (if it has enum type) to a
// default zero value.
AST_MATCHER(Expr, isEmptyInit) {
if (isa<CXXScalarValueInitExpr>(&Node))
return true;
if (isa<ImplicitValueInitExpr>(&Node))
return true;
if (const auto *Init = dyn_cast<InitListExpr>(&Node)) {
if (Init->getNumInits() == 0)
return true;
}
return false;
}

AST_MATCHER(InitListExpr, hasArrayFiller) { return Node.hasArrayFiller(); }

// Check if any type has a "child" type that is an enum without zero value.
// The "child" type can be an array element type or member type of a record
// type (or a recursive combination of these). In this case, if the "root" type
// is statically initialized, the enum component is initialized to zero.
class FindEnumMember : public TypeVisitor<FindEnumMember, bool> {
public:
const EnumType *FoundEnum = nullptr;

bool VisitType(const Type *T) {
const Type *DesT = T->getUnqualifiedDesugaredType();
if (DesT != T)
return Visit(DesT);
return false;
}
bool VisitArrayType(const ArrayType *T) {
return Visit(T->getElementType()->getUnqualifiedDesugaredType());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is calling getUnqualifiedDesugaredType necessary in VisitArrayType, VisitConstantArrayType and the field visitation of VisitRecordType? If they always call forward to VisitType, is it not enough to have the desugaring done only in VisitType? Feel free to correct me if I am wrong about this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks redundant, I have removed getUnqualifiedDesugaredType when Visit is called (but getTypePtr is needed instead).

}
bool VisitConstantArrayType(const ConstantArrayType *T) {
return Visit(T->getElementType()->getUnqualifiedDesugaredType());
}
bool VisitEnumType(const EnumType *T) {
if (isCompleteAndHasNoZeroValue(T->getDecl())) {
FoundEnum = T;
return true;
}
return false;
}
bool VisitRecordType(const RecordType *T) {
const RecordDecl *RD = T->getDecl();
if (RD->isUnion())
return false;
auto VisitField = [this](const FieldDecl *F) {
return Visit(F->getType()->getUnqualifiedDesugaredType());
};
return llvm::any_of(RD->fields(), VisitField);
}
};

} // namespace

InvalidEnumDefaultInitializationCheck::InvalidEnumDefaultInitializationCheck(
StringRef Name, ClangTidyContext *Context)
: ClangTidyCheck(Name, Context) {}

void InvalidEnumDefaultInitializationCheck::registerMatchers(
MatchFinder *Finder) {
auto EnumWithoutZeroValue = enumType(
hasDeclaration(enumDecl(isCompleteAndHasNoZeroValue()).bind("enum")));
auto EnumOrArrayOfEnum = qualType(hasUnqualifiedDesugaredType(
anyOf(EnumWithoutZeroValue,
arrayType(hasElementType(qualType(
hasUnqualifiedDesugaredType(EnumWithoutZeroValue)))))));
Finder->addMatcher(
expr(isEmptyInit(), hasType(EnumOrArrayOfEnum)).bind("expr"), this);

// Array initialization can contain an "array filler" for the (syntactically)
// unspecified elements. This expression is not found by AST matchers and can
// have any type (the array's element type). This is an implicitly generated
// initialization, so if the type contains somewhere an enum without zero
// enumerator, the zero initialization applies here. We search this array
// element type for the specific enum type manually when this matcher matches.
Finder->addMatcher(initListExpr(hasArrayFiller()).bind("array_filler_expr"),
this);
}

void InvalidEnumDefaultInitializationCheck::check(
const MatchFinder::MatchResult &Result) {
const auto *InitExpr = Result.Nodes.getNodeAs<Expr>("expr");
const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>("enum");
if (!InitExpr) {
const auto *InitList =
Result.Nodes.getNodeAs<InitListExpr>("array_filler_expr");
// Initialization of omitted array elements with array filler was found.
// Check the type for enum without zero value.
// FIXME: In this way only one enum-typed value is found, not all of these.
FindEnumMember Finder;
if (!Finder.Visit(InitList->getArrayFiller()->getType().getTypePtr()))
return;
InitExpr = InitList;
Enum = Finder.FoundEnum->getDecl();
}

if (!InitExpr || !Enum)
return;

ASTContext &ACtx = Enum->getASTContext();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const? Same for Parents below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getParents requires it to be non-const.

SourceLocation Loc = InitExpr->getExprLoc();
if (Loc.isInvalid()) {
if (isa<ImplicitValueInitExpr>(InitExpr) || isa<InitListExpr>(InitExpr)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think isa<>() now supports variadic args, so we could use:
isa<ImplicitValueInitExpr, InitListExpr>(InitExpr)

DynTypedNodeList Parents = ACtx.getParents(*InitExpr);
if (Parents.empty())
return;

if (const auto *Ctor = Parents[0].get<CXXConstructorDecl>()) {
// Try to find member initializer with the found expression and get the
// source location from it.
CXXCtorInitializer *const *CtorInit = std::find_if(
Ctor->init_begin(), Ctor->init_end(),
[InitExpr](const CXXCtorInitializer *Init) {
return Init->isMemberInitializer() && Init->getInit() == InitExpr;
});
if (!CtorInit)
return;
Loc = (*CtorInit)->getLParenLoc();
} else if (const auto *InitList = Parents[0].get<InitListExpr>()) {
// The expression may be implicitly generated for an initialization.
// Search for a parent initialization list with valid source location.
while (InitList->getExprLoc().isInvalid()) {
DynTypedNodeList Parents = ACtx.getParents(*InitList);
if (Parents.empty())
return;
InitList = Parents[0].get<InitListExpr>();
if (!InitList)
return;
}
Loc = InitList->getExprLoc();
}
}
// If still not found a source location, omit the warning.
// Ideally all such cases (if they exist) should be handled to make the
// check more precise.
if (Loc.isInvalid())
return;
}
diag(Loc, "enum value of type %0 initialized with invalid value of 0, "
"enum doesn't have a zero-value enumerator")
<< Enum;
diag(Enum->getLocation(), "enum is defined here", DiagnosticIDs::Note);
}

} // namespace clang::tidy::bugprone
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//===--- InvalidEnumDefaultInitializationCheck.h - clang-tidy -*- C++ -*---===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_INVALIDENUMDEFAULTINITIALIZATIONCHECK_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_INVALIDENUMDEFAULTINITIALIZATIONCHECK_H

#include "../ClangTidyCheck.h"

namespace clang::tidy::bugprone {

/// Detects default initialization (to 0) of variables with `enum` type where
/// the enum has no enumerator with value of 0.
///
/// For the user-facing documentation see:
/// http://clang.llvm.org/extra/clang-tidy/checks/bugprone/invalid-enum-default-initialization.html
class InvalidEnumDefaultInitializationCheck : public ClangTidyCheck {
public:
InvalidEnumDefaultInitializationCheck(StringRef Name,
ClangTidyContext *Context);
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is relevant for C, C++ and Objective-C languages as well, so I think you are right to not override the isLanguageVersionSupported method here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLanguageVersionSupported was already removed.


} // namespace clang::tidy::bugprone

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_BUGPRONE_INVALIDENUMDEFAULTINITIALIZATIONCHECK_H
6 changes: 6 additions & 0 deletions clang-tools-extra/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ New checks
pointer and store it as class members without handle the copy and move
constructors and the assignments.

- New :doc:`bugprone-invalid-enum-default-initialization
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase from main.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not use rebase because it may remove commits and make comparison difficult. Instead a merge from main was done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge complicates history and make it non-linear.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the github pull request UI gets confused when commits are rebased or amended on the branch that acts as the "source" of the PR. (IIRC inline comments will get lost, but there may be other problems as well.) For this reason I think it's prudent to use merge commits during the lifetime of a github review.

Note that when the PR is accepted all the commits on it (including the merge commits) will be squashed into a single commit that gets rebased onto the main branch -- so there is no danger of complicating the canonical history of the project. (And the local/internal history of a single PR is small enough that it will remain understandable even if there are merge commits from main.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLVM documentation (https://llvm.org/docs/GitHub.html) actually tells that we should rebase the PR after the review is finished, force push a final commit and merge this on github. (But rebase can involve changes that should be reviewed?) Comparing changes between PR commits (like a diff only of the last update) seems to be not possible after rebase. "Squash and merge" does work (if there is no conflict) and leaves a single (non-merge) commit in the main branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLVM documentation (https://llvm.org/docs/GitHub.html) actually tells that we should rebase the PR after the review is finished, force push a final commit and merge this on github.

The documentation primarily suggest using the "Squash and merge" button and only offers this "Merge using the GitHub command line interface" workflow (that you appear to summarize) as a secondary alternative. Also note that there is almost no difference between using "Squash and merge" and this CLI workflow.

(But rebase can involve changes that should be reviewed?)

Yes, some rebases can involve nontrivial changes, but the intended workflow is that:

  • if the change is not yet compatible with the main branch (a rebase would involve nontrivial changes), then merge the main branch to the PR branch, add the necessary correction commits and review them;
  • once the change is complete and compatible with the main branch (i.e. it can be trivially rebased), it should be squashed and rebased (either by the slightly misnamed "Squash and merge" button, or the equivalent CLI workflow) -- and in this situation the rebase is trivial.

Comparing changes between PR commits (like a diff only of the last update) seems to be not possible after rebase.

Yes, that's a significant reason why rebases during the lifetime of the PR should be avoided.

"Squash and merge" does work (if there is no conflict) and leaves a single (non-merge) commit in the main branch.

Yes, it's a bit unfortunate that they called it "Squash and merge" instead of "Squash and rebase" (which would accurately describe its effects) -- but it works correctly.

<clang-tidy/checks/bugprone/invalid-enum-default-initialization>` check.

Detects default initialization (to 0) of variables with ``enum`` type where
the enum has no enumerator with value of 0.

- New :doc:`bugprone-misleading-setter-of-reference
<clang-tidy/checks/bugprone/misleading-setter-of-reference>` check.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.. title:: clang-tidy - bugprone-invalid-enum-default-initialization

bugprone-invalid-enum-default-initialization
============================================

Detects default initialization (to 0) of variables with ``enum`` type where
the enum has no enumerator with value of 0.

In C++ a default initialization is performed if a variable is initialized with
initializer list or in other implicit ways, and no value is specified at the
initialization. In such cases the value 0 is used for the initialization.
This also applies to enumerations even if it does not have an enumerator with
value 0. In this way a variable with the enum type may contain initially an
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
value 0. In this way a variable with the enum type may contain initially an
value 0. In this way a variable with the ``enum`` type may contain initially an

invalid value (if the program expects that it contains only the listed
enumerator values).

The checker emits a warning only if an enum variable is default-initialized
(contrary to not initialized) and the enum type does not have an enumerator with
value of 0. The enum type can be scoped or non-scoped enum. Unions are not
handled by the check (if it contains a member of enum type).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The checker emits a warning only if an enum variable is default-initialized
(contrary to not initialized) and the enum type does not have an enumerator with
value of 0. The enum type can be scoped or non-scoped enum. Unions are not
handled by the check (if it contains a member of enum type).
The check emits a warning only if an ``enum`` variable is default-initialized
(contrary to not initialized) and the ``enum`` type does not have an enumerator with
value of 0. The ``enum`` type can be scoped or non-scoped ``enum``. Unions are not
handled by the check (if it contains a member of ``enum`` type).


.. code-block:: c++

enum class Enum1: int {
A = 1,
B
};

enum class Enum0: int {
A = 0,
B
};

void f() {
Enum1 X1{}; // warn: 'X1' is initialized to 0
Enum1 X2 = Enum1(); // warn: 'X2' is initialized to 0
Enum1 X3; // no warning: 'X3' is not initialized
Enum0 X4{}; // no warning: type has an enumerator with value of 0
}

struct S1 {
Enum1 A;
S(): A() {} // warn: 'A' is initialized to 0
};

struct S2 {
int A;
Enum1 B;
};

S2 VarS2{}; // warn: member 'B' is initialized to 0

The check applies to initialization of arrays or structures with initialization
lists in C code too. In these cases elements not specified in the list (and have
enum type) are set to 0.

.. code-block:: c
enum Enum1 {
Enum1_A = 1,
Enum1_B
};
struct Struct1 {
int a;
enum Enum1 b;
};
enum Enum1 Array1[2] = {Enum1_A}; // warn: omitted elements are initialized to 0
enum Enum1 Array2[2][2] = {{Enum1_A}, {Enum1_A}}; // warn: last element of both nested arrays is initialized to 0
enum Enum1 Array3[2][2] = {{Enum1_A, Enum1_A}}; // warn: elements of second array are initialized to 0
struct Struct1 S1 = {1}; // warn: element 'b' is initialized to 0
1 change: 1 addition & 0 deletions clang-tools-extra/docs/clang-tidy/checks/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Clang-Tidy Checks
:doc:`bugprone-incorrect-roundings <bugprone/incorrect-roundings>`,
:doc:`bugprone-infinite-loop <bugprone/infinite-loop>`,
:doc:`bugprone-integer-division <bugprone/integer-division>`,
:doc:`bugprone-invalid-enum-default-initialization <bugprone/invalid-enum-default-initialization>`,
:doc:`bugprone-lambda-function-name <bugprone/lambda-function-name>`,
:doc:`bugprone-macro-parentheses <bugprone/macro-parentheses>`, "Yes"
:doc:`bugprone-macro-repeated-side-effects <bugprone/macro-repeated-side-effects>`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// RUN: %check_clang_tidy %s bugprone-invalid-enum-default-initialization %t

enum Enum1 {
Enum1_A = 1,
Enum1_B
};

struct Struct1 {
int a;
enum Enum1 b;
};

struct Struct2 {
struct Struct1 a;
char b;
};

enum Enum1 E1 = {};
// CHECK-NOTES: :[[@LINE-1]]:17: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here
enum Enum1 E2[10] = {};
// CHECK-NOTES: :[[@LINE-1]]:21: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here
enum Enum1 E3[10] = {Enum1_A};
// CHECK-NOTES: :[[@LINE-1]]:21: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here
enum Enum1 E4[2][2] = {{Enum1_A}, {Enum1_A}};
// CHECK-NOTES: :[[@LINE-1]]:24: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here
// CHECK-NOTES: :[[@LINE-3]]:35: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here
enum Enum1 E5[2][2] = {{Enum1_A, Enum1_A}};
// CHECK-NOTES: :[[@LINE-1]]:23: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here


struct Struct1 S1[2][2] = {{{1, Enum1_A}, {2, Enum1_A}}};
// CHECK-NOTES: :[[@LINE-1]]:27: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here

struct Struct2 S2[3] = {{1}};
// CHECK-NOTES: :[[@LINE-1]]:24: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here
// CHECK-NOTES: :[[@LINE-3]]:26: warning: enum value of type 'Enum1' initialized with invalid value of 0, enum doesn't have a zero-value enumerator
// CHECK-NOTES: :3:6: note: enum is defined here

union Union1 {
enum Enum1 a;
int b;
};

// no warnings for union
union Union1 U1 = {};
union Union1 U2[3] = {};
Loading
Loading