[clang-tools-extra] 89f1321 - [clang-tidy] Add check to find calls to NSInvocation methods under ARC that don't have proper object argument lifetimes.

Michael Wyman via cfe-commits cfe-commits at lists.llvm.org
Fri Apr 10 08:51:28 PDT 2020


Author: Michael Wyman
Date: 2020-04-10T08:51:21-07:00
New Revision: 89f1321fe4ef203a4674213280b430a274dc2001

URL: https://github.com/llvm/llvm-project/commit/89f1321fe4ef203a4674213280b430a274dc2001
DIFF: https://github.com/llvm/llvm-project/commit/89f1321fe4ef203a4674213280b430a274dc2001.diff

LOG: [clang-tidy] Add check to find calls to NSInvocation methods under ARC that don't have proper object argument lifetimes.

Summary: This check is similar to an ARC Migration check that warned about this incorrect usage under ARC, but most projects are no longer undergoing migration from pre-ARC code. The documentation for NSInvocation is not explicit about these requirements and incorrect usage has been found in many of our projects.

Reviewers: stephanemoore, benhamilton, dmaclach, alexfh, aaron.ballman, hokein, njames93

Reviewed By: stephanemoore, benhamilton, aaron.ballman

Subscribers: xazax.hun, Eugene.Zelenko, mgorny, cfe-commits

Tags: #clang, #clang-tools-extra

Differential Revision: https://reviews.llvm.org/D77571

Added: 
    clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.cpp
    clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.h
    clang-tools-extra/docs/clang-tidy/checks/objc-nsinvocation-argument-lifetime.rst
    clang-tools-extra/test/clang-tidy/checkers/objc-nsinvocation-argument-lifetime.m

Modified: 
    clang-tools-extra/clang-tidy/objc/CMakeLists.txt
    clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp
    clang-tools-extra/docs/ReleaseNotes.rst
    clang-tools-extra/docs/clang-tidy/checks/list.rst

Removed: 
    


################################################################################
diff  --git a/clang-tools-extra/clang-tidy/objc/CMakeLists.txt b/clang-tools-extra/clang-tidy/objc/CMakeLists.txt
index 9b010f069cde..5c6c5057174f 100644
--- a/clang-tools-extra/clang-tidy/objc/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/objc/CMakeLists.txt
@@ -8,6 +8,7 @@ add_clang_library(clangTidyObjCModule
   DeallocInCategoryCheck.cpp
   ForbiddenSubclassingCheck.cpp
   MissingHashCheck.cpp
+  NSInvocationArgumentLifetimeCheck.cpp
   ObjCTidyModule.cpp
   PropertyDeclarationCheck.cpp
   SuperSelfCheck.cpp

diff  --git a/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.cpp b/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.cpp
new file mode 100644
index 000000000000..520bdc3b9f12
--- /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<FixItHint>
+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<FixItHint>
+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<ParmVarDecl>(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<FixItHint> Hint =
+          fixItHintReplacementForOwnershipString(VarDeclText, Range, WeakText))
+    return Hint;
+
+  if (llvm::Optional<FixItHint> 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<ObjCMessageExpr>("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<VarDecl>("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/NSInvocationArgumentLifetimeCheck.h b/clang-tools-extra/clang-tidy/objc/NSInvocationArgumentLifetimeCheck.h
new file mode 100644
index 000000000000..6d0c30c3a916
--- /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/ObjCTidyModule.cpp b/clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp
index ff220b88df4f..56ab0abc83af 100644
--- 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 @@ class ObjCModule : public ClangTidyModule {
         "objc-forbidden-subclassing");
     CheckFactories.registerCheck<MissingHashCheck>(
         "objc-missing-hash");
+    CheckFactories.registerCheck<NSInvocationArgumentLifetimeCheck>(
+        "objc-nsinvocation-argument-lifetime");
     CheckFactories.registerCheck<PropertyDeclarationCheck>(
         "objc-property-declaration");
     CheckFactories.registerCheck<SuperSelfCheck>(

diff  --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index d6802838b9d1..d3c3bb0520d1 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -92,7 +92,7 @@ New checks
 
   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 @@ New checks
 
   Finds recursive functions and diagnoses them.
 
+- New :doc:`objc-nsinvocation-argument-lifetime
+  <clang-tidy/checks/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 @@ Changes in existing checks
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 - Improved :doc:`readability-qualified-auto
-  <clang-tidy/checks/readability-qualified-auto>` check now supports a 
+  <clang-tidy/checks/readability-qualified-auto>` 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
index 768e10ef714f..a3c695dd3311 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -240,6 +240,7 @@ Clang-Tidy Checks
    `objc-dealloc-in-category <objc-dealloc-in-category.html>`_,
    `objc-forbidden-subclassing <objc-forbidden-subclassing.html>`_,
    `objc-missing-hash <objc-missing-hash.html>`_,
+   `objc-nsinvocation-argument-lifetime <objc-nsinvocation-argument-lifetime.html>`_, "Yes"
    `objc-property-declaration <objc-property-declaration.html>`_, "Yes"
    `objc-super-self <objc-super-self.html>`_, "Yes"
    `openmp-exception-escape <openmp-exception-escape.html>`_,

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
index 000000000000..ae1cc67bdee7
--- /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
index 000000000000..48396e47e94b
--- /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))
+ at interface NSObject
+ at end
+
+ at interface NSInvocation : NSObject
+- (void)getArgument:(void *)Arg atIndex:(int)Index;
+- (void)getReturnValue:(void *)ReturnValue;
+ at end
+
+ at interface OtherClass : NSObject
+- (void)getArgument:(void *)Arg atIndex:(int)Index;
+ at 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];
+}
+
+ at interface TestClass : NSObject {
+ at public
+  id Argument1;
+  __unsafe_unretained id Argument2;
+  struct Foo Bar;
+  int IntIvar;
+}
+ at end
+
+ at 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)];
+}
+
+ at 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];
+}


        


More information about the cfe-commits mailing list