xref: /llvm-project/clang-tools-extra/clang-tidy/bugprone/ForwardDeclarationNamespaceCheck.cpp (revision 670a4613fc5f29036f23fe357b0dbf017d019717)
1 //===--- ForwardDeclarationNamespaceCheck.cpp - 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 #include "ForwardDeclarationNamespaceCheck.h"
10 #include "clang/AST/ASTContext.h"
11 #include "clang/AST/Decl.h"
12 #include "clang/ASTMatchers/ASTMatchFinder.h"
13 #include "clang/ASTMatchers/ASTMatchers.h"
14 #include <stack>
15 #include <string>
16 
17 using namespace clang::ast_matchers;
18 
19 namespace clang::tidy::bugprone {
20 
21 void ForwardDeclarationNamespaceCheck::registerMatchers(MatchFinder *Finder) {
22   // Match all class declarations/definitions *EXCEPT*
23   // 1. implicit classes, e.g. `class A {};` has implicit `class A` inside `A`.
24   // 2. nested classes declared/defined inside another class.
25   // 3. template class declaration, template instantiation or
26   //    specialization (NOTE: extern specialization is filtered out by
27   //    `unless(hasAncestor(cxxRecordDecl()))`).
28   auto IsInSpecialization = hasAncestor(
29       decl(anyOf(cxxRecordDecl(isExplicitTemplateSpecialization()),
30                  functionDecl(isExplicitTemplateSpecialization()))));
31   Finder->addMatcher(
32       cxxRecordDecl(
33           hasParent(decl(anyOf(namespaceDecl(), translationUnitDecl()))),
34           unless(isImplicit()), unless(hasAncestor(cxxRecordDecl())),
35           unless(isInstantiated()), unless(IsInSpecialization),
36           unless(classTemplateSpecializationDecl()))
37           .bind("record_decl"),
38       this);
39 
40   // Match all friend declarations. Classes used in friend declarations are not
41   // marked as referenced in AST. We need to record all record classes used in
42   // friend declarations.
43   Finder->addMatcher(friendDecl().bind("friend_decl"), this);
44 }
45 
46 void ForwardDeclarationNamespaceCheck::check(
47     const MatchFinder::MatchResult &Result) {
48   if (const auto *RecordDecl =
49           Result.Nodes.getNodeAs<CXXRecordDecl>("record_decl")) {
50     StringRef DeclName = RecordDecl->getName();
51     if (RecordDecl->isThisDeclarationADefinition()) {
52       DeclNameToDefinitions[DeclName].push_back(RecordDecl);
53     } else {
54       // If a declaration has no definition, the definition could be in another
55       // namespace (a wrong namespace).
56       // NOTE: even a declaration does have definition, we still need it to
57       // compare with other declarations.
58       DeclNameToDeclarations[DeclName].push_back(RecordDecl);
59     }
60   } else {
61     const auto *Decl = Result.Nodes.getNodeAs<FriendDecl>("friend_decl");
62     assert(Decl && "Decl is neither record_decl nor friend decl!");
63 
64     // Classes used in friend declarations are not marked referenced in AST,
65     // so we need to check classes used in friend declarations manually to
66     // reduce the rate of false positive.
67     // For example, in
68     //    \code
69     //      struct A;
70     //      struct B { friend A; };
71     //    \endcode
72     // `A` will not be marked as "referenced" in the AST.
73     if (const TypeSourceInfo *Tsi = Decl->getFriendType()) {
74       QualType Desugared = Tsi->getType().getDesugaredType(*Result.Context);
75       FriendTypes.insert(Desugared.getTypePtr());
76     }
77   }
78 }
79 
80 static bool haveSameNamespaceOrTranslationUnit(const CXXRecordDecl *Decl1,
81                                                const CXXRecordDecl *Decl2) {
82   const DeclContext *ParentDecl1 = Decl1->getLexicalParent();
83   const DeclContext *ParentDecl2 = Decl2->getLexicalParent();
84 
85   // Since we only matched declarations whose parent is Namespace or
86   // TranslationUnit declaration, the parent should be either a translation unit
87   // or namespace.
88   if (ParentDecl1->getDeclKind() == Decl::TranslationUnit ||
89       ParentDecl2->getDeclKind() == Decl::TranslationUnit) {
90     return ParentDecl1 == ParentDecl2;
91   }
92   assert(ParentDecl1->getDeclKind() == Decl::Namespace &&
93          "ParentDecl1 declaration must be a namespace");
94   assert(ParentDecl2->getDeclKind() == Decl::Namespace &&
95          "ParentDecl2 declaration must be a namespace");
96   auto *Ns1 = NamespaceDecl::castFromDeclContext(ParentDecl1);
97   auto *Ns2 = NamespaceDecl::castFromDeclContext(ParentDecl2);
98   return Ns1->getFirstDecl() == Ns2->getFirstDecl();
99 }
100 
101 static std::string getNameOfNamespace(const CXXRecordDecl *Decl) {
102   const auto *ParentDecl = Decl->getLexicalParent();
103   if (ParentDecl->getDeclKind() == Decl::TranslationUnit) {
104     return "(global)";
105   }
106   const auto *NsDecl = cast<NamespaceDecl>(ParentDecl);
107   std::string Ns;
108   llvm::raw_string_ostream OStream(Ns);
109   NsDecl->printQualifiedName(OStream);
110   return Ns.empty() ? "(global)" : Ns;
111 }
112 
113 void ForwardDeclarationNamespaceCheck::onEndOfTranslationUnit() {
114   // Iterate each group of declarations by name.
115   for (const auto &KeyValuePair : DeclNameToDeclarations) {
116     const auto &Declarations = KeyValuePair.second;
117     // If more than 1 declaration exists, we check if all are in the same
118     // namespace.
119     for (const auto *CurDecl : Declarations) {
120       if (CurDecl->hasDefinition() || CurDecl->isReferenced()) {
121         continue; // Skip forward declarations that are used/referenced.
122       }
123       if (FriendTypes.contains(CurDecl->getTypeForDecl())) {
124         continue; // Skip forward declarations referenced as friend.
125       }
126       if (CurDecl->getLocation().isMacroID() ||
127           CurDecl->getLocation().isInvalid()) {
128         continue;
129       }
130       // Compare with all other declarations with the same name.
131       for (const auto *Decl : Declarations) {
132         if (Decl == CurDecl) {
133           continue; // Don't compare with self.
134         }
135         if (!CurDecl->hasDefinition() &&
136             !haveSameNamespaceOrTranslationUnit(CurDecl, Decl)) {
137           diag(CurDecl->getLocation(),
138                "declaration %0 is never referenced, but a declaration with "
139                "the same name found in another namespace '%1'")
140               << CurDecl << getNameOfNamespace(Decl);
141           diag(Decl->getLocation(), "a declaration of %0 is found here",
142                DiagnosticIDs::Note)
143               << Decl;
144           break; // FIXME: We only generate one warning for each declaration.
145         }
146       }
147       // Check if a definition in another namespace exists.
148       const auto DeclName = CurDecl->getName();
149       auto It = DeclNameToDefinitions.find(DeclName);
150       if (It == DeclNameToDefinitions.end()) {
151         continue; // No definition in this translation unit, we can skip it.
152       }
153       // Make a warning for each definition with the same name (in other
154       // namespaces).
155       const auto &Definitions = It->second;
156       for (const auto *Def : Definitions) {
157         diag(CurDecl->getLocation(),
158              "no definition found for %0, but a definition with "
159              "the same name %1 found in another namespace '%2'")
160             << CurDecl << Def << getNameOfNamespace(Def);
161         diag(Def->getLocation(), "a definition of %0 is found here",
162              DiagnosticIDs::Note)
163             << Def;
164       }
165     }
166   }
167 }
168 
169 } // namespace clang::tidy::bugprone
170