1 /*
2     This file is part of the clazy static checker.
3 
4     Copyright (C) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
5     Author: Sérgio Martins <sergio.martins@kdab.com>
6 
7     Copyright (C) 2015 Sergio Martins <smartins@kde.org>
8 
9     This library is free software; you can redistribute it and/or
10     modify it under the terms of the GNU Library General Public
11     License as published by the Free Software Foundation; either
12     version 2 of the License, or (at your option) any later version.
13 
14     This library is distributed in the hope that it will be useful,
15     but WITHOUT ANY WARRANTY; without even the implied warranty of
16     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17     Library General Public License for more details.
18 
19     You should have received a copy of the GNU Library General Public License
20     along with this library; see the file COPYING.LIB.  If not, write to
21     the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22     Boston, MA 02110-1301, USA.
23 */
24 
25 #include "old-style-connect.h"
26 #include "Utils.h"
27 #include "StringUtils.h"
28 #include "FixItUtils.h"
29 #include "ContextUtils.h"
30 #include "QtUtils.h"
31 #include "ClazyContext.h"
32 #include "AccessSpecifierManager.h"
33 #include "HierarchyUtils.h"
34 #include "SourceCompatibilityHelpers.h"
35 #include "clazy_stl.h"
36 
37 #include <clang/AST/Decl.h>
38 #include <clang/AST/DeclCXX.h>
39 #include <clang/Lex/Lexer.h>
40 #include <clang/AST/DeclBase.h>
41 #include <clang/AST/Expr.h>
42 #include <clang/AST/ExprCXX.h>
43 #include <clang/AST/Stmt.h>
44 #include <clang/AST/Type.h>
45 #include <clang/Basic/Diagnostic.h>
46 #include <clang/Basic/IdentifierTable.h>
47 #include <clang/Basic/LLVM.h>
48 #include <clang/Basic/SourceManager.h>
49 #include <clang/Basic/TokenKinds.h>
50 #include <clang/Lex/Token.h>
51 #include <llvm/ADT/StringRef.h>
52 #include <llvm/Support/Casting.h>
53 #include <llvm/Support/raw_ostream.h>
54 
55 #include <regex>
56 
57 namespace clang {
58 class MacroInfo;
59 }  // namespace clang
60 
61 using namespace clang;
62 using namespace std;
63 
64 namespace clazy {
65 // Copied from Clang's Expr.cpp and added "|| !DerivedType->isRecordType()" to avoid a crash
getBestDynamicClassType(Expr * expr)66 const CXXRecordDecl *getBestDynamicClassType(Expr *expr)
67 {
68     if (!expr)
69         return nullptr;
70 
71     const Expr *E = expr->getBestDynamicClassTypeExpr();
72     QualType DerivedType = E->getType();
73     if (const PointerType *PTy = DerivedType->getAs<PointerType>())
74         DerivedType = PTy->getPointeeType();
75 
76     if (DerivedType->isDependentType() || !DerivedType->isRecordType())
77         return nullptr;
78 
79     const RecordType *Ty = DerivedType->castAs<RecordType>();
80     Decl *D = Ty->getDecl();
81     return cast<CXXRecordDecl>(D);
82 }
83 }
84 
85 enum ConnectFlag {
86     ConnectFlag_None = 0,       // Not a disconnect or connect
87     ConnectFlag_Connect = 1,    // It's a connect
88     ConnectFlag_Disconnect = 2, // It's a disconnect
89     ConnectFlag_QTimerSingleShot = 4,
90     ConnectFlag_OldStyle = 8,   // Qt4 style
91     ConnectFlag_4ArgsDisconnect = 16, // disconnect(const char *signal = 0, const QObject *receiver = 0, const char *method = 0) const
92     ConnectFlag_3ArgsDisconnect = 32, // disconnect(SIGNAL(foo))
93     ConnectFlag_2ArgsDisconnect = 64, //disconnect(const QObject *receiver, const char *method = 0) const
94     ConnectFlag_5ArgsConnect = 128, // connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
95     ConnectFlag_4ArgsConnect = 256, // connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
96     ConnectFlag_OldStyleButNonLiteral = 0x200, // connect(foo, SIGNAL(bar()), foo, variableWithSlotName); // here the slot name isn't a literal
97     ConnectFlag_QStateAddTransition = 0x400,
98     ConnectFlag_QMenuAddAction = 0x800,
99     ConnectFlag_QMessageBoxOpen = 0x1000,
100     ConnectFlag_Bogus = 0x2000
101 };
102 
classIsOk(StringRef className)103 static bool classIsOk(StringRef className)
104 {
105     // List of classes we usually use Qt4 syntax
106     return className != "QDBusInterface";
107 }
108 
OldStyleConnect(const std::string & name,ClazyContext * context)109 OldStyleConnect::OldStyleConnect(const std::string &name, ClazyContext *context)
110     : CheckBase(name, context, Option_CanIgnoreIncludes)
111 {
112     enablePreProcessorCallbacks();
113     context->enableAccessSpecifierManager();
114 }
115 
classifyConnect(FunctionDecl * connectFunc,CallExpr * connectCall)116 int OldStyleConnect::classifyConnect(FunctionDecl *connectFunc, CallExpr *connectCall)
117 {
118     int classification = ConnectFlag_None;
119 
120     const string methodName = connectFunc->getQualifiedNameAsString();
121     if (methodName == "QObject::connect")
122         classification |= ConnectFlag_Connect;
123     else if (methodName == "QObject::disconnect")
124         classification |= ConnectFlag_Disconnect;
125     else if (methodName == "QTimer::singleShot")
126         classification |= ConnectFlag_QTimerSingleShot;
127     else if (methodName == "QState::addTransition")
128         classification |= ConnectFlag_QStateAddTransition;
129     else if (methodName == "QMenu::addAction")
130         classification |= ConnectFlag_QMenuAddAction;
131     else if (methodName == "QMessageBox::open")
132         classification |= ConnectFlag_QMessageBoxOpen;
133 
134     if (classification == ConnectFlag_None)
135         return classification;
136 
137     if (clazy::connectHasPMFStyle(connectFunc))
138         return classification;
139     else
140         classification |= ConnectFlag_OldStyle;
141 
142     const int numParams = connectFunc->getNumParams();
143 
144     if (classification & ConnectFlag_Connect) {
145         if (numParams == 5) {
146             classification |= ConnectFlag_5ArgsConnect;
147         } else if (numParams == 4) {
148             classification |= ConnectFlag_4ArgsConnect;
149         } else {
150             classification |= ConnectFlag_Bogus;
151         }
152     } else if (classification & ConnectFlag_Disconnect) {
153         if (numParams == 4) {
154             classification |= ConnectFlag_4ArgsDisconnect;
155         } else if (numParams == 3) {
156             classification |= ConnectFlag_3ArgsDisconnect;
157         } else if (numParams == 2) {
158             classification |= ConnectFlag_2ArgsDisconnect;
159         } else {
160             classification |= ConnectFlag_Bogus;
161         }
162     }
163 
164     if (classification & ConnectFlag_OldStyle) {
165         // It's old style, but check if all macros are literals
166         int numLiterals = 0;
167         for (auto arg : connectCall->arguments()) {
168             auto argLocation = clazy::getLocStart(arg);
169             string dummy;
170             if (isSignalOrSlot(argLocation, dummy))
171                 ++numLiterals;
172         }
173 
174         if ((classification & ConnectFlag_QTimerSingleShot) && numLiterals != 1) {
175             classification |= ConnectFlag_OldStyleButNonLiteral;
176         } else if (((classification & ConnectFlag_Connect) && numLiterals != 2)) {
177             classification |= ConnectFlag_OldStyleButNonLiteral;
178         } else if ((classification & ConnectFlag_4ArgsDisconnect) && numLiterals != 2)  {
179             classification |= ConnectFlag_OldStyleButNonLiteral;
180         } else if ((classification & ConnectFlag_QStateAddTransition) && numLiterals != 1) {
181             classification |= ConnectFlag_OldStyleButNonLiteral;
182         } else if ((classification & ConnectFlag_Disconnect) && numLiterals == 0) {
183             classification |= ConnectFlag_OldStyleButNonLiteral;
184         } else if ((classification & ConnectFlag_QMenuAddAction) && numLiterals != 1) {
185             classification |= ConnectFlag_OldStyleButNonLiteral;
186         } else if ((classification & ConnectFlag_QMessageBoxOpen) && numLiterals != 1) {
187             classification |= ConnectFlag_OldStyleButNonLiteral;
188         }
189     }
190 
191     return classification;
192 }
193 
isQPointer(Expr * expr) const194 bool OldStyleConnect::isQPointer(Expr *expr) const
195 {
196     vector<CXXMemberCallExpr*> memberCalls;
197     clazy::getChilds<CXXMemberCallExpr>(expr, memberCalls);
198 
199     for (auto callExpr : memberCalls) {
200         if (!callExpr->getDirectCallee())
201             continue;
202         auto method = dyn_cast<CXXMethodDecl>(callExpr->getDirectCallee());
203         if (!method)
204             continue;
205 
206         // Any better way to detect it's an operator ?
207         if (clazy::startsWith(method->getNameAsString(), "operator "))
208             return true;
209     }
210 
211     return false;
212 }
213 
isPrivateSlot(const string & name) const214 bool OldStyleConnect::isPrivateSlot(const string &name) const
215 {
216     return clazy::any_of(m_privateSlots, [name](const PrivateSlot &slot) {
217         return slot.name == name;
218     });
219 }
220 
VisitStmt(Stmt * s)221 void OldStyleConnect::VisitStmt(Stmt *s)
222 {
223     auto call = dyn_cast<CallExpr>(s);
224     if (!call)
225         return;
226 
227     if (m_context->lastMethodDecl && m_context->isQtDeveloper() && m_context->lastMethodDecl->getParent() &&
228         clazy::name(m_context->lastMethodDecl->getParent()) == "QObject") // Don't warn of stuff inside qobject.h
229         return;
230 
231     FunctionDecl *function = call->getDirectCallee();
232     if (!function)
233         return;
234 
235     auto method = dyn_cast<CXXMethodDecl>(function);
236     if (!method)
237         return;
238 
239     const int classification = classifyConnect(method, call);
240     if (!(classification & ConnectFlag_OldStyle))
241         return;
242 
243     if ((classification & ConnectFlag_OldStyleButNonLiteral))
244         return;
245 
246     if (classification & ConnectFlag_Bogus) {
247         emitWarning(clazy::getLocStart(s), "Internal error");
248         return;
249     }
250 
251     emitWarning(clazy::getLocStart(s), "Old Style Connect", fixits(classification, call));
252 }
253 
addPrivateSlot(const PrivateSlot & slot)254 void OldStyleConnect::addPrivateSlot(const PrivateSlot &slot)
255 {
256     m_privateSlots.push_back(slot);
257 }
258 
VisitMacroExpands(const Token & macroNameTok,const SourceRange & range,const MacroInfo *)259 void OldStyleConnect::VisitMacroExpands(const Token &macroNameTok, const SourceRange &range, const MacroInfo *)
260 {
261     IdentifierInfo *ii = macroNameTok.getIdentifierInfo();
262     if (!ii || ii->getName() != "Q_PRIVATE_SLOT")
263         return;
264 
265     auto charRange = Lexer::getAsCharRange(range, sm(), lo());
266     const string text = Lexer::getSourceText(charRange, sm(), lo());
267 
268     static regex rx(R"(Q_PRIVATE_SLOT\s*\((.*)\s*,\s*.*\s+(.*)\(.*)");
269     smatch match;
270     if (!regex_match(text, match, rx) || match.size() != 3)
271         return;
272 
273     addPrivateSlot({match[1], match[2]});
274 }
275 
276 // SIGNAL(foo()) -> foo
signalOrSlotNameFromMacro(SourceLocation macroLoc)277 string OldStyleConnect::signalOrSlotNameFromMacro(SourceLocation macroLoc)
278 {
279     if (!macroLoc.isMacroID())
280         return "error";
281 
282     CharSourceRange expansionRange = clazy::getImmediateExpansionRange(macroLoc, sm());
283     SourceRange range = SourceRange(expansionRange.getBegin(), expansionRange.getEnd());
284     auto charRange = Lexer::getAsCharRange(range, sm(), lo());
285     const string text = Lexer::getSourceText(charRange, sm(), lo());
286 
287     static regex rx(R"(\s*(SIGNAL|SLOT)\s*\(\s*(.+)\s*\(.*)");
288 
289     smatch match;
290     if (regex_match(text, match, rx)) {
291         if (match.size() == 3) {
292             return match[2].str();
293         } else {
294             return "error2";
295         }
296     } else {
297         return string("regexp failed for ") + text;
298     }
299 }
300 
isSignalOrSlot(SourceLocation loc,string & macroName) const301 bool OldStyleConnect::isSignalOrSlot(SourceLocation loc, string &macroName) const
302 {
303     macroName.clear();
304     if (!loc.isMacroID() || loc.isInvalid())
305         return false;
306 
307     macroName = Lexer::getImmediateMacroName(loc, sm(), lo());
308     return macroName == "SIGNAL" || macroName == "SLOT";
309 }
310 
fixits(int classification,CallExpr * call)311 vector<FixItHint> OldStyleConnect::fixits(int classification, CallExpr *call)
312 {
313     if (!isFixitEnabled())
314         return {};
315 
316     if (!call) {
317         llvm::errs() << "Call is invalid\n";
318         return {};
319     }
320 
321     if (classification & ConnectFlag_2ArgsDisconnect) {
322         // Not implemented yet
323         string msg = "Fix it not implemented for disconnect with 2 args";
324         queueManualFixitWarning(clazy::getLocStart(call), msg);
325         return {};
326     }
327 
328     if (classification & ConnectFlag_3ArgsDisconnect) {
329         // Not implemented yet
330         string msg = "Fix it not implemented for disconnect with 3 args";
331         queueManualFixitWarning(clazy::getLocStart(call), msg);
332         return {};
333     }
334 
335     if (classification & ConnectFlag_QMessageBoxOpen) {
336         string msg = "Fix it not implemented for QMessageBox::open()";
337         queueManualFixitWarning(clazy::getLocStart(call), msg);
338         return {};
339     }
340 
341     vector<FixItHint> fixits;
342     int macroNum = 0;
343     string implicitCallee;
344     string macroName;
345     CXXMethodDecl *senderMethod = nullptr;
346     for (auto arg : call->arguments()) {
347         SourceLocation s = clazy::getLocStart(arg);
348         static const CXXRecordDecl *lastRecordDecl = nullptr;
349         if (isSignalOrSlot(s, macroName)) {
350             macroNum++;
351             if (!lastRecordDecl && (classification & ConnectFlag_4ArgsConnect)) {
352                 // This means it's a connect with implicit receiver
353                 lastRecordDecl = Utils::recordForMemberCall(dyn_cast<CXXMemberCallExpr>(call), implicitCallee);
354 
355                 if (macroNum == 1)
356                     llvm::errs() << "This first macro shouldn't enter this path";
357                 if (!lastRecordDecl) {
358                     string msg = "Failed to get class name for implicit receiver";
359                     queueManualFixitWarning(s, msg);
360                     return {};
361                 }
362             }
363 
364             if (!lastRecordDecl) {
365                 string msg = "Failed to get class name for explicit receiver";
366                 queueManualFixitWarning(s, msg);
367                 return {};
368             }
369 
370             const string methodName = signalOrSlotNameFromMacro(s);
371 
372             auto methods = Utils::methodsFromString(lastRecordDecl, methodName);
373             if (methods.empty()) {
374                 string msg;
375                 if (isPrivateSlot(methodName)) {
376                     msg = "Converting Q_PRIVATE_SLOTS not implemented yet\n";
377                 } else {
378                     if (m_context->isQtDeveloper() && classIsOk(clazy::name(lastRecordDecl))) {
379                         // This is OK
380                         return {};
381                     } else {
382                         msg = "No such method " + methodName + " in class " + lastRecordDecl->getNameAsString();
383                     }
384                 }
385 
386                 queueManualFixitWarning(s, msg);
387                 return {};
388             } else if (methods.size() != 1) {
389                 string msg = string("Too many overloads (") + to_string(methods.size()) + string(") for method ")
390                              + methodName + " for record " + lastRecordDecl->getNameAsString();
391                 queueManualFixitWarning(s, msg);
392                 return {};
393             } else {
394                 AccessSpecifierManager *a = m_context->accessSpecifierManager;
395                 if (!a)
396                     return {};
397 
398                 const bool isSignal = a->qtAccessSpecifierType(methods[0]) == QtAccessSpecifier_Signal;
399                 if (isSignal && macroName == "SLOT") {
400                     // The method is actually a signal and the user used SLOT()
401                     // bail out with the fixing.
402                     string msg = string("Can't fix. SLOT macro used but method " + methodName + " is a signal");
403                     queueManualFixitWarning(s, msg);
404                     return {};
405                 }
406             }
407 
408             auto methodDecl = methods[0];
409             if (methodDecl->isStatic())
410                 return {};
411 
412             if (macroNum == 1) {
413                 // Save the number of parameters of the signal. The slot should not have more arguments.
414                 senderMethod = methodDecl;
415             } else if (macroNum == 2) {
416                 const unsigned int numReceiverParams = methodDecl->getNumParams();
417                 if (numReceiverParams > senderMethod->getNumParams()) {
418                     string msg = string("Receiver has more parameters (") + to_string(methodDecl->getNumParams()) + ") than signal (" + to_string(senderMethod->getNumParams()) + ')';
419                     queueManualFixitWarning(s, msg);
420                     return {};
421                 }
422 
423                 for (unsigned int i = 0; i < numReceiverParams; ++i) {
424                     ParmVarDecl *receiverParm = methodDecl->getParamDecl(i);
425                     ParmVarDecl *senderParm = senderMethod->getParamDecl(i);
426                     if (!clazy::isConvertibleTo(senderParm->getType().getTypePtr(), receiverParm->getType().getTypePtrOrNull())) {
427                         string msg = string("Sender's parameters are incompatible with the receiver's");
428                         queueManualFixitWarning(s, msg);
429                         return {};
430                     }
431                 }
432             }
433 
434             if ((classification & ConnectFlag_QTimerSingleShot) && methodDecl->getNumParams() > 0) {
435                 string msg = "(QTimer) Fixit not implemented for slot with arguments, use a lambda";
436                 queueManualFixitWarning(s, msg);
437                 return {};
438             }
439 
440             if ((classification & ConnectFlag_QMenuAddAction) && methodDecl->getNumParams() > 0) {
441                 string msg = "(QMenu) Fixit not implemented for slot with arguments, use a lambda";
442                 queueManualFixitWarning(s, msg);
443                 return {};
444             }
445 
446             DeclContext *context = m_context->lastDecl->getDeclContext();
447 
448             bool isSpecialProtectedCase = false;
449             if (!clazy::canTakeAddressOf(methodDecl, context, /*by-ref*/ isSpecialProtectedCase)) {
450                 string msg = "Can't fix " + clazy::accessString(methodDecl->getAccess()) + ' ' + macroName + ' ' + methodDecl->getQualifiedNameAsString();
451                 queueManualFixitWarning(s, msg);
452                 return {};
453             }
454 
455             string qualifiedName;
456             auto contextRecord = clazy::firstContextOfType<CXXRecordDecl>(m_context->lastDecl->getDeclContext());
457             const bool isInInclude = sm().getMainFileID() != sm().getFileID(clazy::getLocStart(call));
458 
459             if (isSpecialProtectedCase && contextRecord) {
460                 // We're inside a derived class trying to take address of a protected base member, must use &Derived::method instead of &Base::method.
461                 qualifiedName = contextRecord->getNameAsString() + "::" + methodDecl->getNameAsString();
462             } else {
463                 qualifiedName = clazy::getMostNeededQualifiedName(sm(), methodDecl, context, clazy::getLocStart(call), !isInInclude); // (In includes ignore using directives)
464             }
465 
466             CharSourceRange expansionRange = clazy::getImmediateExpansionRange(s, sm());
467             SourceRange range = SourceRange(expansionRange.getBegin(), expansionRange.getEnd());
468 
469             const string functionPointer = '&' + qualifiedName;
470             string replacement = functionPointer;
471 
472             if ((classification & ConnectFlag_4ArgsConnect) && macroNum == 2)
473                 replacement = implicitCallee + ", " + replacement;
474 
475             fixits.push_back(FixItHint::CreateReplacement(range, replacement));
476             lastRecordDecl = nullptr;
477         } else {
478             Expr *expr = arg;
479             const auto record = clazy::getBestDynamicClassType(expr);
480             if (record) {
481                 lastRecordDecl = record;
482                 if (isQPointer(expr)) {
483                     auto endLoc = clazy::locForNextToken(&m_astContext, clazy::getLocStart(arg), tok::comma);
484                     if (endLoc.isValid()) {
485                         fixits.push_back(FixItHint::CreateInsertion(endLoc, ".data()"));
486                     } else {
487                         queueManualFixitWarning(s, "Can't fix this QPointer case");
488                         return {};
489                     }
490                 }
491             }
492         }
493     }
494 
495     return fixits;
496 }
497