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