1 //===--- AvoidNSObjectNewCheck.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 "AvoidNSObjectNewCheck.h"
10 #include "clang/AST/ASTContext.h"
11 #include "clang/ASTMatchers/ASTMatchFinder.h"
12 #include "clang/Basic/LangOptions.h"
13 #include "clang/Basic/SourceLocation.h"
14 #include "clang/Basic/SourceManager.h"
15 #include "llvm/Support/FormatVariadic.h"
16 #include <map>
17 #include <string>
18
19 using namespace clang::ast_matchers;
20
21 namespace clang {
22 namespace tidy {
23 namespace google {
24 namespace objc {
25
isMessageExpressionInsideMacro(const ObjCMessageExpr * Expr)26 static bool isMessageExpressionInsideMacro(const ObjCMessageExpr *Expr) {
27 SourceLocation ReceiverLocation = Expr->getReceiverRange().getBegin();
28 if (ReceiverLocation.isMacroID())
29 return true;
30
31 SourceLocation SelectorLocation = Expr->getSelectorStartLoc();
32 if (SelectorLocation.isMacroID())
33 return true;
34
35 return false;
36 }
37
38 // Walk up the class hierarchy looking for an -init method, returning true
39 // if one is found and has not been marked unavailable.
isInitMethodAvailable(const ObjCInterfaceDecl * ClassDecl)40 static bool isInitMethodAvailable(const ObjCInterfaceDecl *ClassDecl) {
41 while (ClassDecl != nullptr) {
42 for (const auto *MethodDecl : ClassDecl->instance_methods()) {
43 if (MethodDecl->getSelector().getAsString() == "init")
44 return !MethodDecl->isUnavailable();
45 }
46 ClassDecl = ClassDecl->getSuperClass();
47 }
48
49 // No -init method found in the class hierarchy. This should occur only rarely
50 // in Objective-C code, and only really applies to classes not derived from
51 // NSObject.
52 return false;
53 }
54
55 // Returns the string for the Objective-C message receiver. Keeps any generics
56 // included in the receiver class type, which are stripped if the class type is
57 // used. While the generics arguments will not make any difference to the
58 // returned code at this time, the style guide allows them and they should be
59 // left in any fix-it hint.
getReceiverString(SourceRange ReceiverRange,const SourceManager & SM,const LangOptions & LangOpts)60 static StringRef getReceiverString(SourceRange ReceiverRange,
61 const SourceManager &SM,
62 const LangOptions &LangOpts) {
63 CharSourceRange CharRange = Lexer::makeFileCharRange(
64 CharSourceRange::getTokenRange(ReceiverRange), SM, LangOpts);
65 return Lexer::getSourceText(CharRange, SM, LangOpts);
66 }
67
getCallFixItHint(const ObjCMessageExpr * Expr,const SourceManager & SM,const LangOptions & LangOpts)68 static FixItHint getCallFixItHint(const ObjCMessageExpr *Expr,
69 const SourceManager &SM,
70 const LangOptions &LangOpts) {
71 // Check whether the messaged class has a known factory method to use instead
72 // of -init.
73 StringRef Receiver =
74 getReceiverString(Expr->getReceiverRange(), SM, LangOpts);
75 // Some classes should use standard factory methods instead of alloc/init.
76 std::map<StringRef, StringRef> ClassToFactoryMethodMap = {{"NSDate", "date"},
77 {"NSNull", "null"}};
78 auto FoundClassFactory = ClassToFactoryMethodMap.find(Receiver);
79 if (FoundClassFactory != ClassToFactoryMethodMap.end()) {
80 StringRef ClassName = FoundClassFactory->first;
81 StringRef FactorySelector = FoundClassFactory->second;
82 std::string NewCall =
83 llvm::formatv("[{0} {1}]", ClassName, FactorySelector);
84 return FixItHint::CreateReplacement(Expr->getSourceRange(), NewCall);
85 }
86
87 if (isInitMethodAvailable(Expr->getReceiverInterface())) {
88 std::string NewCall = llvm::formatv("[[{0} alloc] init]", Receiver);
89 return FixItHint::CreateReplacement(Expr->getSourceRange(), NewCall);
90 }
91
92 return {}; // No known replacement available.
93 }
94
registerMatchers(MatchFinder * Finder)95 void AvoidNSObjectNewCheck::registerMatchers(MatchFinder *Finder) {
96 if (!getLangOpts().ObjC)
97 return;
98
99 // Add two matchers, to catch calls to +new and implementations of +new.
100 Finder->addMatcher(
101 objcMessageExpr(isClassMessage(), hasSelector("new")).bind("new_call"),
102 this);
103 Finder->addMatcher(
104 objcMethodDecl(isClassMethod(), isDefinition(), hasName("new"))
105 .bind("new_override"),
106 this);
107 }
108
check(const MatchFinder::MatchResult & Result)109 void AvoidNSObjectNewCheck::check(const MatchFinder::MatchResult &Result) {
110 if (const auto *CallExpr =
111 Result.Nodes.getNodeAs<ObjCMessageExpr>("new_call")) {
112 // Don't warn if the call expression originates from a macro expansion.
113 if (isMessageExpressionInsideMacro(CallExpr))
114 return;
115
116 diag(CallExpr->getExprLoc(), "do not create objects with +new")
117 << getCallFixItHint(CallExpr, *Result.SourceManager,
118 Result.Context->getLangOpts());
119 }
120
121 if (const auto *DeclExpr =
122 Result.Nodes.getNodeAs<ObjCMethodDecl>("new_override")) {
123 diag(DeclExpr->getBeginLoc(), "classes should not override +new");
124 }
125 }
126
127 } // namespace objc
128 } // namespace google
129 } // namespace tidy
130 } // namespace clang
131