| 1 | //===--- SlicingCheck.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 "SlicingCheck.h" |
| 10 | #include "clang/AST/ASTContext.h" |
| 11 | #include "clang/AST/RecordLayout.h" |
| 12 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
| 13 | #include "clang/ASTMatchers/ASTMatchers.h" |
| 14 | |
| 15 | using namespace clang::ast_matchers; |
| 16 | |
| 17 | namespace clang::tidy::cppcoreguidelines { |
| 18 | |
| 19 | void SlicingCheck::registerMatchers(MatchFinder *Finder) { |
| 20 | // When we see: |
| 21 | // class B : public A { ... }; |
| 22 | // A a; |
| 23 | // B b; |
| 24 | // a = b; |
| 25 | // The assignment is OK if: |
| 26 | // - the assignment operator is defined as taking a B as second parameter, |
| 27 | // or |
| 28 | // - B does not define any additional members (either variables or |
| 29 | // overrides) wrt A. |
| 30 | // |
| 31 | // The same holds for copy ctor calls. This also captures stuff like: |
| 32 | // void f(A a); |
| 33 | // f(b); |
| 34 | |
| 35 | // Helpers. |
| 36 | const auto OfBaseClass = ofClass(InnerMatcher: cxxRecordDecl().bind(ID: "BaseDecl" )); |
| 37 | const auto IsDerivedFromBaseDecl = |
| 38 | cxxRecordDecl(isDerivedFrom(Base: equalsBoundNode(ID: "BaseDecl" ))) |
| 39 | .bind(ID: "DerivedDecl" ); |
| 40 | const auto HasTypeDerivedFromBaseDecl = |
| 41 | anyOf(hasType(InnerMatcher: IsDerivedFromBaseDecl), |
| 42 | hasType(InnerMatcher: references(InnerMatcher: IsDerivedFromBaseDecl))); |
| 43 | const auto IsCallToBaseClass = hasParent(cxxConstructorDecl( |
| 44 | ofClass(InnerMatcher: isSameOrDerivedFrom(Base: equalsBoundNode(ID: "DerivedDecl" ))), |
| 45 | hasAnyConstructorInitializer(InnerMatcher: allOf( |
| 46 | isBaseInitializer(), withInitializer(InnerMatcher: equalsBoundNode(ID: "Call" )))))); |
| 47 | |
| 48 | // Assignment slicing: "a = b;" and "a = std::move(b);" variants. |
| 49 | const auto SlicesObjectInAssignment = |
| 50 | callExpr(expr().bind(ID: "Call" ), |
| 51 | callee(InnerMatcher: cxxMethodDecl(anyOf(isCopyAssignmentOperator(), |
| 52 | isMoveAssignmentOperator()), |
| 53 | OfBaseClass)), |
| 54 | hasArgument(N: 1, InnerMatcher: HasTypeDerivedFromBaseDecl)); |
| 55 | |
| 56 | // Construction slicing: "A a{b};" and "f(b);" variants. Note that in case of |
| 57 | // slicing the letter will create a temporary and therefore call a ctor. |
| 58 | const auto SlicesObjectInCtor = cxxConstructExpr( |
| 59 | expr().bind(ID: "Call" ), |
| 60 | hasDeclaration(InnerMatcher: cxxConstructorDecl( |
| 61 | anyOf(isCopyConstructor(), isMoveConstructor()), OfBaseClass)), |
| 62 | hasArgument(N: 0, InnerMatcher: HasTypeDerivedFromBaseDecl), |
| 63 | // We need to disable matching on the call to the base copy/move |
| 64 | // constructor in DerivedDecl's constructors. |
| 65 | unless(IsCallToBaseClass)); |
| 66 | |
| 67 | Finder->addMatcher( |
| 68 | NodeMatch: traverse(TK: TK_AsIs, InnerMatcher: expr(SlicesObjectInAssignment).bind(ID: "Call" )), Action: this); |
| 69 | Finder->addMatcher(NodeMatch: traverse(TK: TK_AsIs, InnerMatcher: SlicesObjectInCtor), Action: this); |
| 70 | } |
| 71 | |
| 72 | /// Warns on methods overridden in DerivedDecl with respect to BaseDecl. |
| 73 | /// FIXME: this warns on all overrides outside of the sliced path in case of |
| 74 | /// multiple inheritance. |
| 75 | void SlicingCheck::diagnoseSlicedOverriddenMethods( |
| 76 | const Expr &Call, const CXXRecordDecl &DerivedDecl, |
| 77 | const CXXRecordDecl &BaseDecl) { |
| 78 | if (DerivedDecl.getCanonicalDecl() == BaseDecl.getCanonicalDecl()) |
| 79 | return; |
| 80 | for (const auto *Method : DerivedDecl.methods()) { |
| 81 | // Virtual destructors are OK. We're ignoring constructors since they are |
| 82 | // tagged as overrides. |
| 83 | if (isa<CXXConstructorDecl>(Val: Method) || isa<CXXDestructorDecl>(Val: Method)) |
| 84 | continue; |
| 85 | if (Method->size_overridden_methods() > 0) { |
| 86 | diag(Loc: Call.getExprLoc(), |
| 87 | Description: "slicing object from type %0 to %1 discards override %2" ) |
| 88 | << &DerivedDecl << &BaseDecl << Method; |
| 89 | } |
| 90 | } |
| 91 | // Recursively process bases. |
| 92 | for (const auto &Base : DerivedDecl.bases()) { |
| 93 | if (const auto *BaseRecordType = Base.getType()->getAs<RecordType>()) { |
| 94 | if (const auto *BaseRecord = cast_or_null<CXXRecordDecl>( |
| 95 | Val: BaseRecordType->getDecl()->getDefinition())) |
| 96 | diagnoseSlicedOverriddenMethods(Call, DerivedDecl: *BaseRecord, BaseDecl); |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | void SlicingCheck::check(const MatchFinder::MatchResult &Result) { |
| 102 | const auto *BaseDecl = Result.Nodes.getNodeAs<CXXRecordDecl>(ID: "BaseDecl" ); |
| 103 | const auto *DerivedDecl = |
| 104 | Result.Nodes.getNodeAs<CXXRecordDecl>(ID: "DerivedDecl" ); |
| 105 | const auto *Call = Result.Nodes.getNodeAs<Expr>(ID: "Call" ); |
| 106 | assert(BaseDecl != nullptr); |
| 107 | assert(DerivedDecl != nullptr); |
| 108 | assert(Call != nullptr); |
| 109 | |
| 110 | // Warn when slicing the vtable. |
| 111 | // We're looking through all the methods in the derived class and see if they |
| 112 | // override some methods in the base class. |
| 113 | // It's not enough to just test whether the class is polymorphic because we |
| 114 | // would be fine slicing B to A if no method in B (or its bases) overrides |
| 115 | // anything in A: |
| 116 | // class A { virtual void f(); }; |
| 117 | // class B : public A {}; |
| 118 | // because in that case calling A::f is the same as calling B::f. |
| 119 | diagnoseSlicedOverriddenMethods(Call: *Call, DerivedDecl: *DerivedDecl, BaseDecl: *BaseDecl); |
| 120 | |
| 121 | // Warn when slicing member variables. |
| 122 | const auto &BaseLayout = |
| 123 | BaseDecl->getASTContext().getASTRecordLayout(BaseDecl); |
| 124 | const auto &DerivedLayout = |
| 125 | DerivedDecl->getASTContext().getASTRecordLayout(DerivedDecl); |
| 126 | const CharUnits StateSize = |
| 127 | DerivedLayout.getDataSize() - BaseLayout.getDataSize(); |
| 128 | if (StateSize.isPositive()) { |
| 129 | diag(Loc: Call->getExprLoc(), Description: "slicing object from type %0 to %1 discards " |
| 130 | "%2 bytes of state" ) |
| 131 | << DerivedDecl << BaseDecl << static_cast<int>(StateSize.getQuantity()); |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | } // namespace clang::tidy::cppcoreguidelines |
| 136 | |