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