//===--- OwningMemoryCheck.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 "OwningMemoryCheck.h" #include "../utils/Matchers.h" #include "../utils/OptionsUtils.h" #include "clang/AST/ASTContext.h" #include "clang/ASTMatchers/ASTMatchFinder.h" #include #include using namespace clang::ast_matchers; using namespace clang::ast_matchers::internal; namespace clang { namespace tidy { namespace cppcoreguidelines { // FIXME: Copied from 'NoMallocCheck.cpp'. Has to be refactored into 'util' or // something like that. namespace { Matcher hasAnyListedName(const std::string &FunctionNames) { const std::vector NameList = utils::options::parseStringList(FunctionNames); return hasAnyName(std::vector(NameList.begin(), NameList.end())); } } // namespace void OwningMemoryCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { Options.store(Opts, "LegacyResourceProducers", LegacyResourceProducers); Options.store(Opts, "LegacyResourceConsumers", LegacyResourceConsumers); } /// Match common cases, where the owner semantic is relevant, like function /// calls, delete expressions and others. void OwningMemoryCheck::registerMatchers(MatchFinder *Finder) { const auto OwnerDecl = typeAliasTemplateDecl(hasName("::gsl::owner")); const auto IsOwnerType = hasType(OwnerDecl); const auto LegacyCreatorFunctions = hasAnyListedName(LegacyResourceProducers); const auto LegacyConsumerFunctions = hasAnyListedName(LegacyResourceConsumers); // Legacy functions that are use for resource management but cannot be // updated to use `gsl::owner<>`, like standard C memory management. const auto CreatesLegacyOwner = callExpr(callee(functionDecl(LegacyCreatorFunctions))); // C-style functions like `::malloc()` sometimes create owners as void* // which is expected to be cast to the correct type in C++. This case // must be catched explicitly. const auto LegacyOwnerCast = castExpr(hasSourceExpression(CreatesLegacyOwner)); // Functions that do manual resource management but cannot be updated to use // owner. Best example is `::free()`. const auto LegacyOwnerConsumers = functionDecl(LegacyConsumerFunctions); const auto CreatesOwner = anyOf(cxxNewExpr(), callExpr(callee( functionDecl(returns(qualType(hasDeclaration(OwnerDecl)))))), CreatesLegacyOwner, LegacyOwnerCast); const auto ConsideredOwner = eachOf(IsOwnerType, CreatesOwner); // Find delete expressions that delete non-owners. Finder->addMatcher( traverse(ast_type_traits::TK_AsIs, cxxDeleteExpr(hasDescendant(declRefExpr(unless(ConsideredOwner)) .bind("deleted_variable"))) .bind("delete_expr")), this); // Ignoring the implicit casts is vital because the legacy owners do not work // with the 'owner<>' annotation and therefore always implicitly cast to the // legacy type (even 'void *'). // // Furthermore, legacy owner functions are assumed to use raw pointers for // resources. This check assumes that all pointer arguments of a legacy // functions shall be 'gsl::owner<>'. Finder->addMatcher( traverse(ast_type_traits::TK_AsIs, callExpr(callee(LegacyOwnerConsumers), hasAnyArgument( expr(unless(ignoringImpCasts(ConsideredOwner)), hasType(pointerType())))) .bind("legacy_consumer")), this); // Matching assignment to owners, with the rhs not being an owner nor creating // one. Finder->addMatcher( traverse(ast_type_traits::TK_AsIs, binaryOperator(isAssignmentOperator(), hasLHS(IsOwnerType), hasRHS(unless(ConsideredOwner))) .bind("owner_assignment")), this); // Matching initialization of owners with non-owners, nor creating owners. Finder->addMatcher( traverse(ast_type_traits::TK_AsIs, namedDecl( varDecl(hasInitializer(unless(ConsideredOwner)), IsOwnerType) .bind("owner_initialization"))), this); const auto HasConstructorInitializerForOwner = has(cxxConstructorDecl(forEachConstructorInitializer( cxxCtorInitializer( isMemberInitializer(), forField(IsOwnerType), withInitializer( // Avoid templatesdeclaration with // excluding parenListExpr. allOf(unless(ConsideredOwner), unless(parenListExpr())))) .bind("owner_member_initializer")))); // Match class member initialization that expects owners, but does not get // them. Finder->addMatcher(traverse(ast_type_traits::TK_AsIs, cxxRecordDecl(HasConstructorInitializerForOwner)), this); // Matching on assignment operations where the RHS is a newly created owner, // but the LHS is not an owner. Finder->addMatcher(binaryOperator(isAssignmentOperator(), hasLHS(unless(IsOwnerType)), hasRHS(CreatesOwner)) .bind("bad_owner_creation_assignment"), this); // Matching on initialization operations where the initial value is a newly // created owner, but the LHS is not an owner. Finder->addMatcher( traverse( ast_type_traits::TK_AsIs, namedDecl( varDecl(eachOf(allOf(hasInitializer(CreatesOwner), unless(IsOwnerType)), allOf(hasInitializer(ConsideredOwner), hasType(autoType().bind("deduced_type"))))) .bind("bad_owner_creation_variable"))), this); // Match on all function calls that expect owners as arguments, but didn't // get them. Finder->addMatcher( callExpr(forEachArgumentWithParam( expr(unless(ConsideredOwner)).bind("expected_owner_argument"), parmVarDecl(IsOwnerType))), this); // Matching for function calls where one argument is a created owner, but the // parameter type is not an owner. Finder->addMatcher(callExpr(forEachArgumentWithParam( expr(CreatesOwner).bind("bad_owner_creation_argument"), parmVarDecl(unless(IsOwnerType)) .bind("bad_owner_creation_parameter"))), this); // Matching on functions, that return an owner/resource, but don't declare // their return type as owner. Finder->addMatcher( functionDecl(hasDescendant(returnStmt(hasReturnValue(ConsideredOwner)) .bind("bad_owner_return")), unless(returns(qualType(hasDeclaration(OwnerDecl))))) .bind("function_decl"), this); // Match on classes that have an owner as member, but don't declare a // destructor to properly release the owner. Finder->addMatcher( cxxRecordDecl( has(fieldDecl(IsOwnerType).bind("undestructed_owner_member")), anyOf(unless(has(cxxDestructorDecl())), has(cxxDestructorDecl(anyOf(isDefaulted(), isDeleted()))))) .bind("non_destructor_class"), this); } void OwningMemoryCheck::check(const MatchFinder::MatchResult &Result) { const auto &Nodes = Result.Nodes; bool CheckExecuted = false; CheckExecuted |= handleDeletion(Nodes); CheckExecuted |= handleLegacyConsumers(Nodes); CheckExecuted |= handleExpectedOwner(Nodes); CheckExecuted |= handleAssignmentAndInit(Nodes); CheckExecuted |= handleAssignmentFromNewOwner(Nodes); CheckExecuted |= handleReturnValues(Nodes); CheckExecuted |= handleOwnerMembers(Nodes); assert(CheckExecuted && "None of the subroutines executed, logic error in matcher!"); } bool OwningMemoryCheck::handleDeletion(const BoundNodes &Nodes) { // Result of delete matchers. const auto *DeleteStmt = Nodes.getNodeAs("delete_expr"); const auto *DeletedVariable = Nodes.getNodeAs("deleted_variable"); // Deletion of non-owners, with `delete variable;` if (DeleteStmt) { diag(DeleteStmt->getBeginLoc(), "deleting a pointer through a type that is " "not marked 'gsl::owner<>'; consider using a " "smart pointer instead") << DeletedVariable->getSourceRange(); // FIXME: The declaration of the variable that was deleted can be // rewritten. const ValueDecl *Decl = DeletedVariable->getDecl(); diag(Decl->getBeginLoc(), "variable declared here", DiagnosticIDs::Note) << Decl->getSourceRange(); return true; } return false; } bool OwningMemoryCheck::handleLegacyConsumers(const BoundNodes &Nodes) { // Result of matching for legacy consumer-functions like `::free()`. const auto *LegacyConsumer = Nodes.getNodeAs("legacy_consumer"); // FIXME: `freopen` should be handled separately because it takes the filename // as a pointer, which should not be an owner. The argument that is an owner // is known and the false positive coming from the filename can be avoided. if (LegacyConsumer) { diag(LegacyConsumer->getBeginLoc(), "calling legacy resource function without passing a 'gsl::owner<>'") << LegacyConsumer->getSourceRange(); return true; } return false; } bool OwningMemoryCheck::handleExpectedOwner(const BoundNodes &Nodes) { // Result of function call matchers. const auto *ExpectedOwner = Nodes.getNodeAs("expected_owner_argument"); // Expected function argument to be owner. if (ExpectedOwner) { diag(ExpectedOwner->getBeginLoc(), "expected argument of type 'gsl::owner<>'; got %0") << ExpectedOwner->getType() << ExpectedOwner->getSourceRange(); return true; } return false; } /// Assignment and initialization of owner variables. bool OwningMemoryCheck::handleAssignmentAndInit(const BoundNodes &Nodes) { const auto *OwnerAssignment = Nodes.getNodeAs("owner_assignment"); const auto *OwnerInitialization = Nodes.getNodeAs("owner_initialization"); const auto *OwnerInitializer = Nodes.getNodeAs("owner_member_initializer"); // Assignments to owners. if (OwnerAssignment) { diag(OwnerAssignment->getBeginLoc(), "expected assignment source to be of type 'gsl::owner<>'; got %0") << OwnerAssignment->getRHS()->getType() << OwnerAssignment->getSourceRange(); return true; } // Initialization of owners. if (OwnerInitialization) { diag(OwnerInitialization->getBeginLoc(), "expected initialization with value of type 'gsl::owner<>'; got %0") << OwnerInitialization->getAnyInitializer()->getType() << OwnerInitialization->getSourceRange(); return true; } // Initializer of class constructors that initialize owners. if (OwnerInitializer) { diag(OwnerInitializer->getSourceLocation(), "expected initialization of owner member variable with value of type " "'gsl::owner<>'; got %0") // FIXME: the expression from getInit has type 'void', but the type // of the supplied argument would be of interest. << OwnerInitializer->getInit()->getType() << OwnerInitializer->getSourceRange(); return true; } return false; } /// Problematic assignment and initializations, since the assigned value is a /// newly created owner. bool OwningMemoryCheck::handleAssignmentFromNewOwner(const BoundNodes &Nodes) { const auto *BadOwnerAssignment = Nodes.getNodeAs("bad_owner_creation_assignment"); const auto *BadOwnerInitialization = Nodes.getNodeAs("bad_owner_creation_variable"); const auto *BadOwnerArgument = Nodes.getNodeAs("bad_owner_creation_argument"); const auto *BadOwnerParameter = Nodes.getNodeAs("bad_owner_creation_parameter"); // Bad assignments to non-owners, where the RHS is a newly created owner. if (BadOwnerAssignment) { diag(BadOwnerAssignment->getBeginLoc(), "assigning newly created 'gsl::owner<>' to non-owner %0") << BadOwnerAssignment->getLHS()->getType() << BadOwnerAssignment->getSourceRange(); return true; } // Bad initialization of non-owners, where the RHS is a newly created owner. if (BadOwnerInitialization) { diag(BadOwnerInitialization->getBeginLoc(), "initializing non-owner %0 with a newly created 'gsl::owner<>'") << BadOwnerInitialization->getType() << BadOwnerInitialization->getSourceRange(); // FIXME: FixitHint to rewrite the type of the initialized variable // as 'gsl::owner' // If the type of the variable was deduced, the wrapping owner typedef is // eliminated, therefore the check emits a special note for that case. if (Nodes.getNodeAs("deduced_type")) { diag(BadOwnerInitialization->getBeginLoc(), "type deduction did not result in an owner", DiagnosticIDs::Note); } return true; } // Function call, where one arguments is a newly created owner, but the // parameter type is not. if (BadOwnerArgument) { assert(BadOwnerParameter && "parameter for the problematic argument not found"); diag(BadOwnerArgument->getBeginLoc(), "initializing non-owner argument of " "type %0 with a newly created " "'gsl::owner<>'") << BadOwnerParameter->getType() << BadOwnerArgument->getSourceRange(); return true; } return false; } bool OwningMemoryCheck::handleReturnValues(const BoundNodes &Nodes) { // Function return statements, that are owners/resources, but the function // declaration does not declare its return value as owner. const auto *BadReturnType = Nodes.getNodeAs("bad_owner_return"); const auto *Function = Nodes.getNodeAs("function_decl"); // Function return values, that should be owners but aren't. if (BadReturnType) { // The returned value is a resource or variable that was not annotated with // owner<> and the function return type is not owner<>. diag(BadReturnType->getBeginLoc(), "returning a newly created resource of " "type %0 or 'gsl::owner<>' from a " "function whose return type is not 'gsl::owner<>'") << Function->getReturnType() << BadReturnType->getSourceRange(); // FIXME: Rewrite the return type as 'gsl::owner' return true; } return false; } bool OwningMemoryCheck::handleOwnerMembers(const BoundNodes &Nodes) { // Classes, that have owners as member, but do not declare destructors // accordingly. const auto *BadClass = Nodes.getNodeAs("non_destructor_class"); // Classes, that contains owners, but do not declare destructors. if (BadClass) { const auto *DeclaredOwnerMember = Nodes.getNodeAs("undestructed_owner_member"); assert(DeclaredOwnerMember && "match on class with bad destructor but without a declared owner"); diag(DeclaredOwnerMember->getBeginLoc(), "member variable of type 'gsl::owner<>' requires the class %0 to " "implement a destructor to release the owned resource") << BadClass; return true; } return false; } } // namespace cppcoreguidelines } // namespace tidy } // namespace clang