diff --git a/clang-tools-extra/clang-tidy/objc/CMakeLists.txt b/clang-tools-extra/clang-tidy/objc/CMakeLists.txt --- a/clang-tools-extra/clang-tidy/objc/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/objc/CMakeLists.txt @@ -8,6 +8,7 @@ DeallocInCategoryCheck.cpp ForbiddenSubclassingCheck.cpp MissingHashCheck.cpp + NSInvocationArgumentLifetimeCheck.cpp ObjCTidyModule.cpp PropertyDeclarationCheck.cpp SuperSelfCheck.cpp diff --git a/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.h b/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.h @@ -0,0 +1,39 @@ +//===--- NSInvocationArgumentLifetimeCheck.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_OBJC_NSINVOCATIONARGUMENTLIFETIMECHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_OBJC_NSINVOCATIONARGUMENTLIFETIMECHECK_H + +#include "../ClangTidyCheck.h" +#include "clang/Basic/LangStandard.h" + +namespace clang { +namespace tidy { +namespace objc { + +/// Finds calls to NSInvocation methods under ARC that don't have proper +/// argument object lifetimes. +/// +/// For the user-facing documentation see: +/// http://clang.llvm.org/extra/clang-tidy/checks/objc-nsinvocation-argument-lifetime.html +class NSInvocationArgumentLifetimeCheck : public ClangTidyCheck { +public: + NSInvocationArgumentLifetimeCheck(StringRef Name, ClangTidyContext *Context) + : ClangTidyCheck(Name, Context) {} + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return LangOpts.ObjC && LangOpts.ObjCAutoRefCount; + } + void registerMatchers(ast_matchers::MatchFinder *Finder) override; + void check(const ast_matchers::MatchFinder::MatchResult &Result) override; +}; + +} // namespace objc +} // namespace tidy +} // namespace clang + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_OBJC_NSINVOCATIONARGUMENTLIFETIMECHECK_H diff --git a/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.cpp b/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.cpp @@ -0,0 +1,146 @@ +//===--- NSInvocationArgumentLifetimeCheck.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 "NSInvocationArgumentLifetimeCheck.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Attrs.inc" +#include "clang/AST/ComputeDependence.h" +#include "clang/AST/Decl.h" +#include "clang/AST/Expr.h" +#include "clang/AST/ExprObjC.h" +#include "clang/AST/Type.h" +#include "clang/AST/TypeLoc.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/ASTMatchers/ASTMatchersMacros.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/LLVM.h" +#include "clang/Basic/LangOptions.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Basic/SourceManager.h" +#include "llvm/ADT/None.h" +#include "llvm/ADT/Optional.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang::ast_matchers; + +namespace clang { +namespace tidy { +namespace objc { +namespace { + +static constexpr StringRef WeakText = "__weak"; +static constexpr StringRef StrongText = "__strong"; +static constexpr StringRef UnsafeUnretainedText = "__unsafe_unretained"; + +/// Matches ObjCIvarRefExpr, DeclRefExpr, or MemberExpr that reference +/// Objective-C object (or block) variables or fields whose object lifetimes +/// are not __unsafe_unretained. +AST_POLYMORPHIC_MATCHER(isObjCManagedLifetime, + AST_POLYMORPHIC_SUPPORTED_TYPES(ObjCIvarRefExpr, + DeclRefExpr, + MemberExpr)) { + QualType QT = Node.getType(); + return QT->isScalarType() && + (QT->getScalarTypeKind() == Type::STK_ObjCObjectPointer || + QT->getScalarTypeKind() == Type::STK_BlockPointer) && + QT.getQualifiers().getObjCLifetime() > Qualifiers::OCL_ExplicitNone; +} + +static llvm::Optional +fixItHintReplacementForOwnershipString(StringRef Text, CharSourceRange Range, + StringRef Ownership) { + size_t Index = Text.find(Ownership); + if (Index == StringRef::npos) + return llvm::None; + + SourceLocation Begin = Range.getBegin().getLocWithOffset(Index); + SourceLocation End = Begin.getLocWithOffset(Ownership.size()); + return FixItHint::CreateReplacement(SourceRange(Begin, End), + UnsafeUnretainedText); +} + +static llvm::Optional +fixItHintForVarDecl(const VarDecl *VD, const SourceManager &SM, + const LangOptions &LangOpts) { + assert(VD && "VarDecl parameter must not be null"); + // Don't provide fix-its for any parameter variables at this time. + if (isa(VD)) + return llvm::None; + + // Currently there is no way to directly get the source range for the + // __weak/__strong ObjC lifetime qualifiers, so it's necessary to string + // search in the source code. + CharSourceRange Range = Lexer::makeFileCharRange( + CharSourceRange::getTokenRange(VD->getSourceRange()), SM, LangOpts); + if (Range.isInvalid()) { + // An invalid range likely means inside a macro, in which case don't supply + // a fix-it. + return llvm::None; + } + + StringRef VarDeclText = Lexer::getSourceText(Range, SM, LangOpts); + if (llvm::Optional Hint = + fixItHintReplacementForOwnershipString(VarDeclText, Range, WeakText)) + return Hint; + + if (llvm::Optional Hint = fixItHintReplacementForOwnershipString( + VarDeclText, Range, StrongText)) + return Hint; + + return FixItHint::CreateInsertion(Range.getBegin(), "__unsafe_unretained "); +} + +} // namespace + +void NSInvocationArgumentLifetimeCheck::registerMatchers(MatchFinder *Finder) { + Finder->addMatcher( + objcMessageExpr( + hasReceiverType(asString("NSInvocation *")), + anyOf(hasSelector("getArgument:atIndex:"), + hasSelector("getReturnValue:")), + hasArgument( + 0, anyOf(hasDescendant(memberExpr(isObjCManagedLifetime())), + hasDescendant(objcIvarRefExpr(isObjCManagedLifetime())), + hasDescendant( + // Reference to variables, but when dereferencing + // to ivars/fields a more-descendent variable + // reference (e.g. self) may match with strong + // object lifetime, leading to an incorrect match. + // Exclude these conditions. + declRefExpr(to(varDecl().bind("var")), + unless(hasParent(implicitCastExpr())), + isObjCManagedLifetime()))))) + .bind("call"), + this); +} + +void NSInvocationArgumentLifetimeCheck::check( + const MatchFinder::MatchResult &Result) { + const auto *MatchedExpr = Result.Nodes.getNodeAs("call"); + + auto Diag = diag(MatchedExpr->getArg(0)->getBeginLoc(), + "NSInvocation %objcinstance0 should only pass pointers to " + "objects with ownership __unsafe_unretained") + << MatchedExpr->getSelector(); + + // Only provide fix-it hints for references to local variables; fixes for + // instance variable references don't have as clear an automated fix. + const auto *VD = Result.Nodes.getNodeAs("var"); + if (!VD) + return; + + if (auto Hint = fixItHintForVarDecl(VD, *Result.SourceManager, + Result.Context->getLangOpts())) + Diag << *Hint; +} + +} // namespace objc +} // namespace tidy +} // namespace clang diff --git a/clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp b/clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp --- a/clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp @@ -13,6 +13,7 @@ #include "DeallocInCategoryCheck.h" #include "ForbiddenSubclassingCheck.h" #include "MissingHashCheck.h" +#include "NSInvocationArgumentLifetimeCheck.h" #include "PropertyDeclarationCheck.h" #include "SuperSelfCheck.h" @@ -33,6 +34,8 @@ "objc-forbidden-subclassing"); CheckFactories.registerCheck( "objc-missing-hash"); + CheckFactories.registerCheck( + "objc-nsinvocation-argument-lifetime"); CheckFactories.registerCheck( "objc-property-declaration"); CheckFactories.registerCheck( diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -92,7 +92,7 @@ Finds ``cnd_wait``, ``cnd_timedwait``, ``wait``, ``wait_for``, or ``wait_until`` function calls when the function is not invoked from a loop - that checks whether a condition predicate holds or the function has a + that checks whether a condition predicate holds or the function has a condition parameter. - New :doc:`bugprone-reserved-identifier @@ -134,6 +134,12 @@ Finds recursive functions and diagnoses them. +- New :doc:`objc-nsinvocation-argument-lifetime + ` check. + + Finds calls to ``NSInvocation`` methods under ARC that don't have proper + argument object lifetimes. + New check aliases ^^^^^^^^^^^^^^^^^ @@ -161,7 +167,7 @@ ^^^^^^^^^^^^^^^^^^^^^^^^^^ - Improved :doc:`readability-qualified-auto - ` check now supports a + ` check now supports a `AddConstToQualified` to enable adding ``const`` qualifiers to variables typed with ``auto *`` and ``auto &``. diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -240,6 +240,7 @@ `objc-dealloc-in-category `_, `objc-forbidden-subclassing `_, `objc-missing-hash `_, + `objc-nsinvocation-argument-lifetime `_, "Yes" `objc-property-declaration `_, "Yes" `objc-super-self `_, "Yes" `openmp-exception-escape `_, diff --git a/clang-tools-extra/docs/clang-tidy/checks/objc-nsinvocation-argument-lifetime.rst b/clang-tools-extra/docs/clang-tidy/checks/objc-nsinvocation-argument-lifetime.rst new file mode 100644 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/objc-nsinvocation-argument-lifetime.rst @@ -0,0 +1,39 @@ +.. title:: clang-tidy - objc-nsinvocation-argument-lifetime + +objc-nsinvocation-argument-lifetime +=================================== + +Finds calls to ``NSInvocation`` methods under ARC that don't have proper +argument object lifetimes. When passing Objective-C objects as parameters +to the ``NSInvocation`` methods ``getArgument:atIndex:`` and +``getReturnValue:``, the values are copied by value into the argument pointer, +which leads to to incorrect releasing behavior if the object pointers are +not declared ``__unsafe_unretained``. + +For code: + +.. code-block:: objc + + id arg; + [invocation getArgument:&arg atIndex:2]; + + __strong id returnValue; + [invocation getReturnValue:&returnValue]; + +The fix will be: + +.. code-block:: objc + + __unsafe_unretained id arg; + [invocation getArgument:&arg atIndex:2]; + + __unsafe_unretained id returnValue; + [invocation getReturnValue:&returnValue]; + +The check will warn on being passed instance variable references that have +lifetimes other than ``__unsafe_unretained``, but does not propose a fix: + +.. code-block:: objc + + // "id _returnValue" is declaration of instance variable of class. + [invocation getReturnValue:&self->_returnValue]; diff --git a/clang-tools-extra/test/clang-tidy/checkers/objc-nsinvocation-argument-lifetime.m b/clang-tools-extra/test/clang-tidy/checkers/objc-nsinvocation-argument-lifetime.m new file mode 100644 --- /dev/null +++ b/clang-tools-extra/test/clang-tidy/checkers/objc-nsinvocation-argument-lifetime.m @@ -0,0 +1,103 @@ +// RUN: %check_clang_tidy %s objc-nsinvocation-argument-lifetime %t + +__attribute__((objc_root_class)) +@interface NSObject +@end + +@interface NSInvocation : NSObject +- (void)getArgument:(void *)Arg atIndex:(int)Index; +- (void)getReturnValue:(void *)ReturnValue; +@end + +@interface OtherClass : NSObject +- (void)getArgument:(void *)Arg atIndex:(int)Index; +@end + +struct Foo { + __unsafe_unretained id Field1; + id Field2; + int IntField; +}; + +void foo(NSInvocation *Invocation) { + __unsafe_unretained id Arg2; + id Arg3; + // CHECK-FIXES: __unsafe_unretained id Arg3; + NSObject __strong *Arg4; + // CHECK-FIXES: NSObject __unsafe_unretained *Arg4; + __weak id Arg5; + // CHECK-FIXES: __unsafe_unretained id Arg5; + id ReturnValue; + // CHECK-FIXES: __unsafe_unretained id ReturnValue; + void (^BlockArg1)(); + // CHECK-FIXES: __unsafe_unretained void (^BlockArg1)(); + __unsafe_unretained void (^BlockArg2)(); + int IntVar; + struct Foo Bar; + + [Invocation getArgument:&Arg2 atIndex:2]; + [Invocation getArgument:&IntVar atIndex:2]; + + [Invocation getArgument:&Arg3 atIndex:3]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + + [Invocation getArgument:&Arg4 atIndex:4]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + + [Invocation getArgument:&Arg5 atIndex:5]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + + [Invocation getArgument:&BlockArg1 atIndex:6]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + + [Invocation getArgument:&BlockArg2 atIndex:6]; + + [Invocation getReturnValue:&ReturnValue]; + // CHECK-MESSAGES: :[[@LINE-1]]:30: warning: NSInvocation '-getReturnValue:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + + [Invocation getArgument:(void *)0 atIndex:6]; + + [Invocation getArgument:&Bar.Field1 atIndex:2]; + [Invocation getArgument:&Bar.Field2 atIndex:2]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + [Invocation getArgument:&Bar.IntField atIndex:2]; +} + +void bar(OtherClass *OC) { + id Arg; + [OC getArgument:&Arg atIndex:2]; +} + +@interface TestClass : NSObject { +@public + id Argument1; + __unsafe_unretained id Argument2; + struct Foo Bar; + int IntIvar; +} +@end + +@implementation TestClass + +- (void)processInvocation:(NSInvocation *)Invocation { + [Invocation getArgument:&Argument1 atIndex:2]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + [Invocation getArgument:&self->Argument1 atIndex:2]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + [Invocation getArgument:&Argument2 atIndex:2]; + [Invocation getArgument:&self->Argument2 atIndex:2]; + [Invocation getArgument:&self->IntIvar atIndex:2]; + + [Invocation getReturnValue:&(self->Bar.Field1)]; + [Invocation getReturnValue:&(self->Bar.Field2)]; + // CHECK-MESSAGES: :[[@LINE-1]]:30: warning: NSInvocation '-getReturnValue:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + [Invocation getReturnValue:&(self->Bar.IntField)]; +} + +@end + +void baz(NSInvocation *Invocation, TestClass *Obj) { + [Invocation getArgument:&Obj->Argument1 atIndex:2]; + // CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime] + [Invocation getArgument:&Obj->Argument2 atIndex:2]; +}