//===--- SlicingCheck.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 "SlicingCheck.h" #include "clang/AST/ASTContext.h" #include "clang/AST/RecordLayout.h" #include "clang/ASTMatchers/ASTMatchFinder.h" #include "clang/ASTMatchers/ASTMatchers.h" using namespace clang::ast_matchers; namespace clang::tidy::cppcoreguidelines { void SlicingCheck::registerMatchers(MatchFinder *Finder) { // When we see: // class B : public A { ... }; // A a; // B b; // a = b; // The assignment is OK if: // - the assignment operator is defined as taking a B as second parameter, // or // - B does not define any additional members (either variables or // overrides) wrt A. // // The same holds for copy ctor calls. This also captures stuff like: // void f(A a); // f(b); // Helpers. const auto OfBaseClass = ofClass(cxxRecordDecl().bind("BaseDecl")); const auto IsDerivedFromBaseDecl = cxxRecordDecl(isDerivedFrom(equalsBoundNode("BaseDecl"))) .bind("DerivedDecl"); const auto HasTypeDerivedFromBaseDecl = anyOf(hasType(IsDerivedFromBaseDecl), hasType(references(IsDerivedFromBaseDecl))); const auto IsCallToBaseClass = hasParent(cxxConstructorDecl( ofClass(isSameOrDerivedFrom(equalsBoundNode("DerivedDecl"))), hasAnyConstructorInitializer(allOf( isBaseInitializer(), withInitializer(equalsBoundNode("Call")))))); // Assignment slicing: "a = b;" and "a = std::move(b);" variants. const auto SlicesObjectInAssignment = callExpr(expr().bind("Call"), callee(cxxMethodDecl(anyOf(isCopyAssignmentOperator(), isMoveAssignmentOperator()), OfBaseClass)), hasArgument(1, HasTypeDerivedFromBaseDecl)); // Construction slicing: "A a{b};" and "f(b);" variants. Note that in case of // slicing the letter will create a temporary and therefore call a ctor. const auto SlicesObjectInCtor = cxxConstructExpr( expr().bind("Call"), hasDeclaration(cxxConstructorDecl( anyOf(isCopyConstructor(), isMoveConstructor()), OfBaseClass)), hasArgument(0, HasTypeDerivedFromBaseDecl), // We need to disable matching on the call to the base copy/move // constructor in DerivedDecl's constructors. unless(IsCallToBaseClass)); Finder->addMatcher( traverse(TK_AsIs, expr(SlicesObjectInAssignment).bind("Call")), this); Finder->addMatcher(traverse(TK_AsIs, SlicesObjectInCtor), this); } /// Warns on methods overridden in DerivedDecl with respect to BaseDecl. /// FIXME: this warns on all overrides outside of the sliced path in case of /// multiple inheritance. void SlicingCheck::diagnoseSlicedOverriddenMethods( const Expr &Call, const CXXRecordDecl &DerivedDecl, const CXXRecordDecl &BaseDecl) { if (DerivedDecl.getCanonicalDecl() == BaseDecl.getCanonicalDecl()) return; for (const auto *Method : DerivedDecl.methods()) { // Virtual destructors are OK. We're ignoring constructors since they are // tagged as overrides. if (isa(Method) || isa(Method)) continue; if (Method->size_overridden_methods() > 0) { diag(Call.getExprLoc(), "slicing object from type %0 to %1 discards override %2") << &DerivedDecl << &BaseDecl << Method; } } // Recursively process bases. for (const auto &Base : DerivedDecl.bases()) { if (const auto *BaseRecordType = Base.getType()->getAs()) { if (const auto *BaseRecord = cast_or_null( BaseRecordType->getDecl()->getDefinition())) diagnoseSlicedOverriddenMethods(Call, *BaseRecord, BaseDecl); } } } void SlicingCheck::check(const MatchFinder::MatchResult &Result) { const auto *BaseDecl = Result.Nodes.getNodeAs("BaseDecl"); const auto *DerivedDecl = Result.Nodes.getNodeAs("DerivedDecl"); const auto *Call = Result.Nodes.getNodeAs("Call"); assert(BaseDecl != nullptr); assert(DerivedDecl != nullptr); assert(Call != nullptr); // Warn when slicing the vtable. // We're looking through all the methods in the derived class and see if they // override some methods in the base class. // It's not enough to just test whether the class is polymorphic because we // would be fine slicing B to A if no method in B (or its bases) overrides // anything in A: // class A { virtual void f(); }; // class B : public A {}; // because in that case calling A::f is the same as calling B::f. diagnoseSlicedOverriddenMethods(*Call, *DerivedDecl, *BaseDecl); // Warn when slicing member variables. const auto &BaseLayout = BaseDecl->getASTContext().getASTRecordLayout(BaseDecl); const auto &DerivedLayout = DerivedDecl->getASTContext().getASTRecordLayout(DerivedDecl); const CharUnits StateSize = DerivedLayout.getDataSize() - BaseLayout.getDataSize(); if (StateSize.isPositive()) { diag(Call->getExprLoc(), "slicing object from type %0 to %1 discards " "%2 bytes of state") << DerivedDecl << BaseDecl << static_cast(StateSize.getQuantity()); } } } // namespace clang::tidy::cppcoreguidelines