Skip to content

Commit 89f1321

Browse files
committed
[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
1 parent 65b8b64 commit 89f1321

File tree

8 files changed

+340
-2
lines changed

8 files changed

+340
-2
lines changed

clang-tools-extra/clang-tidy/objc/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ add_clang_library(clangTidyObjCModule
88
DeallocInCategoryCheck.cpp
99
ForbiddenSubclassingCheck.cpp
1010
MissingHashCheck.cpp
11+
NSInvocationArgumentLifetimeCheck.cpp
1112
ObjCTidyModule.cpp
1213
PropertyDeclarationCheck.cpp
1314
SuperSelfCheck.cpp
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//===--- NSInvocationArgumentLifetimeCheck.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 "NSInvocationArgumentLifetimeCheck.h"
10+
#include "clang/AST/ASTContext.h"
11+
#include "clang/AST/Attrs.inc"
12+
#include "clang/AST/ComputeDependence.h"
13+
#include "clang/AST/Decl.h"
14+
#include "clang/AST/Expr.h"
15+
#include "clang/AST/ExprObjC.h"
16+
#include "clang/AST/Type.h"
17+
#include "clang/AST/TypeLoc.h"
18+
#include "clang/ASTMatchers/ASTMatchFinder.h"
19+
#include "clang/ASTMatchers/ASTMatchers.h"
20+
#include "clang/ASTMatchers/ASTMatchersMacros.h"
21+
#include "clang/Basic/Diagnostic.h"
22+
#include "clang/Basic/LLVM.h"
23+
#include "clang/Basic/LangOptions.h"
24+
#include "clang/Basic/SourceLocation.h"
25+
#include "clang/Basic/SourceManager.h"
26+
#include "llvm/ADT/None.h"
27+
#include "llvm/ADT/Optional.h"
28+
#include "llvm/ADT/StringRef.h"
29+
#include "llvm/Support/raw_ostream.h"
30+
31+
using namespace clang::ast_matchers;
32+
33+
namespace clang {
34+
namespace tidy {
35+
namespace objc {
36+
namespace {
37+
38+
static constexpr StringRef WeakText = "__weak";
39+
static constexpr StringRef StrongText = "__strong";
40+
static constexpr StringRef UnsafeUnretainedText = "__unsafe_unretained";
41+
42+
/// Matches ObjCIvarRefExpr, DeclRefExpr, or MemberExpr that reference
43+
/// Objective-C object (or block) variables or fields whose object lifetimes
44+
/// are not __unsafe_unretained.
45+
AST_POLYMORPHIC_MATCHER(isObjCManagedLifetime,
46+
AST_POLYMORPHIC_SUPPORTED_TYPES(ObjCIvarRefExpr,
47+
DeclRefExpr,
48+
MemberExpr)) {
49+
QualType QT = Node.getType();
50+
return QT->isScalarType() &&
51+
(QT->getScalarTypeKind() == Type::STK_ObjCObjectPointer ||
52+
QT->getScalarTypeKind() == Type::STK_BlockPointer) &&
53+
QT.getQualifiers().getObjCLifetime() > Qualifiers::OCL_ExplicitNone;
54+
}
55+
56+
static llvm::Optional<FixItHint>
57+
fixItHintReplacementForOwnershipString(StringRef Text, CharSourceRange Range,
58+
StringRef Ownership) {
59+
size_t Index = Text.find(Ownership);
60+
if (Index == StringRef::npos)
61+
return llvm::None;
62+
63+
SourceLocation Begin = Range.getBegin().getLocWithOffset(Index);
64+
SourceLocation End = Begin.getLocWithOffset(Ownership.size());
65+
return FixItHint::CreateReplacement(SourceRange(Begin, End),
66+
UnsafeUnretainedText);
67+
}
68+
69+
static llvm::Optional<FixItHint>
70+
fixItHintForVarDecl(const VarDecl *VD, const SourceManager &SM,
71+
const LangOptions &LangOpts) {
72+
assert(VD && "VarDecl parameter must not be null");
73+
// Don't provide fix-its for any parameter variables at this time.
74+
if (isa<ParmVarDecl>(VD))
75+
return llvm::None;
76+
77+
// Currently there is no way to directly get the source range for the
78+
// __weak/__strong ObjC lifetime qualifiers, so it's necessary to string
79+
// search in the source code.
80+
CharSourceRange Range = Lexer::makeFileCharRange(
81+
CharSourceRange::getTokenRange(VD->getSourceRange()), SM, LangOpts);
82+
if (Range.isInvalid()) {
83+
// An invalid range likely means inside a macro, in which case don't supply
84+
// a fix-it.
85+
return llvm::None;
86+
}
87+
88+
StringRef VarDeclText = Lexer::getSourceText(Range, SM, LangOpts);
89+
if (llvm::Optional<FixItHint> Hint =
90+
fixItHintReplacementForOwnershipString(VarDeclText, Range, WeakText))
91+
return Hint;
92+
93+
if (llvm::Optional<FixItHint> Hint = fixItHintReplacementForOwnershipString(
94+
VarDeclText, Range, StrongText))
95+
return Hint;
96+
97+
return FixItHint::CreateInsertion(Range.getBegin(), "__unsafe_unretained ");
98+
}
99+
100+
} // namespace
101+
102+
void NSInvocationArgumentLifetimeCheck::registerMatchers(MatchFinder *Finder) {
103+
Finder->addMatcher(
104+
objcMessageExpr(
105+
hasReceiverType(asString("NSInvocation *")),
106+
anyOf(hasSelector("getArgument:atIndex:"),
107+
hasSelector("getReturnValue:")),
108+
hasArgument(
109+
0, anyOf(hasDescendant(memberExpr(isObjCManagedLifetime())),
110+
hasDescendant(objcIvarRefExpr(isObjCManagedLifetime())),
111+
hasDescendant(
112+
// Reference to variables, but when dereferencing
113+
// to ivars/fields a more-descendent variable
114+
// reference (e.g. self) may match with strong
115+
// object lifetime, leading to an incorrect match.
116+
// Exclude these conditions.
117+
declRefExpr(to(varDecl().bind("var")),
118+
unless(hasParent(implicitCastExpr())),
119+
isObjCManagedLifetime())))))
120+
.bind("call"),
121+
this);
122+
}
123+
124+
void NSInvocationArgumentLifetimeCheck::check(
125+
const MatchFinder::MatchResult &Result) {
126+
const auto *MatchedExpr = Result.Nodes.getNodeAs<ObjCMessageExpr>("call");
127+
128+
auto Diag = diag(MatchedExpr->getArg(0)->getBeginLoc(),
129+
"NSInvocation %objcinstance0 should only pass pointers to "
130+
"objects with ownership __unsafe_unretained")
131+
<< MatchedExpr->getSelector();
132+
133+
// Only provide fix-it hints for references to local variables; fixes for
134+
// instance variable references don't have as clear an automated fix.
135+
const auto *VD = Result.Nodes.getNodeAs<VarDecl>("var");
136+
if (!VD)
137+
return;
138+
139+
if (auto Hint = fixItHintForVarDecl(VD, *Result.SourceManager,
140+
Result.Context->getLangOpts()))
141+
Diag << *Hint;
142+
}
143+
144+
} // namespace objc
145+
} // namespace tidy
146+
} // namespace clang
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//===--- NSInvocationArgumentLifetimeCheck.h - clang-tidy -------*- C++ -*-===//
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+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_OBJC_NSINVOCATIONARGUMENTLIFETIMECHECK_H
10+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_OBJC_NSINVOCATIONARGUMENTLIFETIMECHECK_H
11+
12+
#include "../ClangTidyCheck.h"
13+
#include "clang/Basic/LangStandard.h"
14+
15+
namespace clang {
16+
namespace tidy {
17+
namespace objc {
18+
19+
/// Finds calls to NSInvocation methods under ARC that don't have proper
20+
/// argument object lifetimes.
21+
///
22+
/// For the user-facing documentation see:
23+
/// http://clang.llvm.org/extra/clang-tidy/checks/objc-nsinvocation-argument-lifetime.html
24+
class NSInvocationArgumentLifetimeCheck : public ClangTidyCheck {
25+
public:
26+
NSInvocationArgumentLifetimeCheck(StringRef Name, ClangTidyContext *Context)
27+
: ClangTidyCheck(Name, Context) {}
28+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
29+
return LangOpts.ObjC && LangOpts.ObjCAutoRefCount;
30+
}
31+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
32+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
33+
};
34+
35+
} // namespace objc
36+
} // namespace tidy
37+
} // namespace clang
38+
39+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_OBJC_NSINVOCATIONARGUMENTLIFETIMECHECK_H

clang-tools-extra/clang-tidy/objc/ObjCTidyModule.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "DeallocInCategoryCheck.h"
1414
#include "ForbiddenSubclassingCheck.h"
1515
#include "MissingHashCheck.h"
16+
#include "NSInvocationArgumentLifetimeCheck.h"
1617
#include "PropertyDeclarationCheck.h"
1718
#include "SuperSelfCheck.h"
1819

@@ -33,6 +34,8 @@ class ObjCModule : public ClangTidyModule {
3334
"objc-forbidden-subclassing");
3435
CheckFactories.registerCheck<MissingHashCheck>(
3536
"objc-missing-hash");
37+
CheckFactories.registerCheck<NSInvocationArgumentLifetimeCheck>(
38+
"objc-nsinvocation-argument-lifetime");
3639
CheckFactories.registerCheck<PropertyDeclarationCheck>(
3740
"objc-property-declaration");
3841
CheckFactories.registerCheck<SuperSelfCheck>(

clang-tools-extra/docs/ReleaseNotes.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ New checks
9292

9393
Finds ``cnd_wait``, ``cnd_timedwait``, ``wait``, ``wait_for``, or
9494
``wait_until`` function calls when the function is not invoked from a loop
95-
that checks whether a condition predicate holds or the function has a
95+
that checks whether a condition predicate holds or the function has a
9696
condition parameter.
9797

9898
- New :doc:`bugprone-reserved-identifier
@@ -134,6 +134,12 @@ New checks
134134

135135
Finds recursive functions and diagnoses them.
136136

137+
- New :doc:`objc-nsinvocation-argument-lifetime
138+
<clang-tidy/checks/objc-nsinvocation-argument-lifetime>` check.
139+
140+
Finds calls to ``NSInvocation`` methods under ARC that don't have proper
141+
argument object lifetimes.
142+
137143
New check aliases
138144
^^^^^^^^^^^^^^^^^
139145

@@ -161,7 +167,7 @@ Changes in existing checks
161167
^^^^^^^^^^^^^^^^^^^^^^^^^^
162168

163169
- Improved :doc:`readability-qualified-auto
164-
<clang-tidy/checks/readability-qualified-auto>` check now supports a
170+
<clang-tidy/checks/readability-qualified-auto>` check now supports a
165171
`AddConstToQualified` to enable adding ``const`` qualifiers to variables
166172
typed with ``auto *`` and ``auto &``.
167173

clang-tools-extra/docs/clang-tidy/checks/list.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ Clang-Tidy Checks
240240
`objc-dealloc-in-category <objc-dealloc-in-category.html>`_,
241241
`objc-forbidden-subclassing <objc-forbidden-subclassing.html>`_,
242242
`objc-missing-hash <objc-missing-hash.html>`_,
243+
`objc-nsinvocation-argument-lifetime <objc-nsinvocation-argument-lifetime.html>`_, "Yes"
243244
`objc-property-declaration <objc-property-declaration.html>`_, "Yes"
244245
`objc-super-self <objc-super-self.html>`_, "Yes"
245246
`openmp-exception-escape <openmp-exception-escape.html>`_,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.. title:: clang-tidy - objc-nsinvocation-argument-lifetime
2+
3+
objc-nsinvocation-argument-lifetime
4+
===================================
5+
6+
Finds calls to ``NSInvocation`` methods under ARC that don't have proper
7+
argument object lifetimes. When passing Objective-C objects as parameters
8+
to the ``NSInvocation`` methods ``getArgument:atIndex:`` and
9+
``getReturnValue:``, the values are copied by value into the argument pointer,
10+
which leads to to incorrect releasing behavior if the object pointers are
11+
not declared ``__unsafe_unretained``.
12+
13+
For code:
14+
15+
.. code-block:: objc
16+
17+
id arg;
18+
[invocation getArgument:&arg atIndex:2];
19+
20+
__strong id returnValue;
21+
[invocation getReturnValue:&returnValue];
22+
23+
The fix will be:
24+
25+
.. code-block:: objc
26+
27+
__unsafe_unretained id arg;
28+
[invocation getArgument:&arg atIndex:2];
29+
30+
__unsafe_unretained id returnValue;
31+
[invocation getReturnValue:&returnValue];
32+
33+
The check will warn on being passed instance variable references that have
34+
lifetimes other than ``__unsafe_unretained``, but does not propose a fix:
35+
36+
.. code-block:: objc
37+
38+
// "id _returnValue" is declaration of instance variable of class.
39+
[invocation getReturnValue:&self->_returnValue];
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// RUN: %check_clang_tidy %s objc-nsinvocation-argument-lifetime %t
2+
3+
__attribute__((objc_root_class))
4+
@interface NSObject
5+
@end
6+
7+
@interface NSInvocation : NSObject
8+
- (void)getArgument:(void *)Arg atIndex:(int)Index;
9+
- (void)getReturnValue:(void *)ReturnValue;
10+
@end
11+
12+
@interface OtherClass : NSObject
13+
- (void)getArgument:(void *)Arg atIndex:(int)Index;
14+
@end
15+
16+
struct Foo {
17+
__unsafe_unretained id Field1;
18+
id Field2;
19+
int IntField;
20+
};
21+
22+
void foo(NSInvocation *Invocation) {
23+
__unsafe_unretained id Arg2;
24+
id Arg3;
25+
// CHECK-FIXES: __unsafe_unretained id Arg3;
26+
NSObject __strong *Arg4;
27+
// CHECK-FIXES: NSObject __unsafe_unretained *Arg4;
28+
__weak id Arg5;
29+
// CHECK-FIXES: __unsafe_unretained id Arg5;
30+
id ReturnValue;
31+
// CHECK-FIXES: __unsafe_unretained id ReturnValue;
32+
void (^BlockArg1)();
33+
// CHECK-FIXES: __unsafe_unretained void (^BlockArg1)();
34+
__unsafe_unretained void (^BlockArg2)();
35+
int IntVar;
36+
struct Foo Bar;
37+
38+
[Invocation getArgument:&Arg2 atIndex:2];
39+
[Invocation getArgument:&IntVar atIndex:2];
40+
41+
[Invocation getArgument:&Arg3 atIndex:3];
42+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
43+
44+
[Invocation getArgument:&Arg4 atIndex:4];
45+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
46+
47+
[Invocation getArgument:&Arg5 atIndex:5];
48+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
49+
50+
[Invocation getArgument:&BlockArg1 atIndex:6];
51+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
52+
53+
[Invocation getArgument:&BlockArg2 atIndex:6];
54+
55+
[Invocation getReturnValue:&ReturnValue];
56+
// CHECK-MESSAGES: :[[@LINE-1]]:30: warning: NSInvocation '-getReturnValue:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
57+
58+
[Invocation getArgument:(void *)0 atIndex:6];
59+
60+
[Invocation getArgument:&Bar.Field1 atIndex:2];
61+
[Invocation getArgument:&Bar.Field2 atIndex:2];
62+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
63+
[Invocation getArgument:&Bar.IntField atIndex:2];
64+
}
65+
66+
void bar(OtherClass *OC) {
67+
id Arg;
68+
[OC getArgument:&Arg atIndex:2];
69+
}
70+
71+
@interface TestClass : NSObject {
72+
@public
73+
id Argument1;
74+
__unsafe_unretained id Argument2;
75+
struct Foo Bar;
76+
int IntIvar;
77+
}
78+
@end
79+
80+
@implementation TestClass
81+
82+
- (void)processInvocation:(NSInvocation *)Invocation {
83+
[Invocation getArgument:&Argument1 atIndex:2];
84+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
85+
[Invocation getArgument:&self->Argument1 atIndex:2];
86+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
87+
[Invocation getArgument:&Argument2 atIndex:2];
88+
[Invocation getArgument:&self->Argument2 atIndex:2];
89+
[Invocation getArgument:&self->IntIvar atIndex:2];
90+
91+
[Invocation getReturnValue:&(self->Bar.Field1)];
92+
[Invocation getReturnValue:&(self->Bar.Field2)];
93+
// CHECK-MESSAGES: :[[@LINE-1]]:30: warning: NSInvocation '-getReturnValue:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
94+
[Invocation getReturnValue:&(self->Bar.IntField)];
95+
}
96+
97+
@end
98+
99+
void baz(NSInvocation *Invocation, TestClass *Obj) {
100+
[Invocation getArgument:&Obj->Argument1 atIndex:2];
101+
// CHECK-MESSAGES: :[[@LINE-1]]:27: warning: NSInvocation '-getArgument:atIndex:' should only pass pointers to objects with ownership __unsafe_unretained [objc-nsinvocation-argument-lifetime]
102+
[Invocation getArgument:&Obj->Argument2 atIndex:2];
103+
}

0 commit comments

Comments
 (0)