1 //===--- HeaderSourceSwitch.cpp - --------------------------------*- 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 "HeaderSourceSwitch.h"
10 #include "AST.h"
11 #include "SourceCode.h"
12 #include "index/SymbolCollector.h"
13 #include "support/Logger.h"
14 #include "clang/AST/Decl.h"
15 
16 namespace clang {
17 namespace clangd {
18 
getCorrespondingHeaderOrSource(const Path & OriginalFile,llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS)19 llvm::Optional<Path> getCorrespondingHeaderOrSource(
20     const Path &OriginalFile,
21     llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS) {
22   llvm::StringRef SourceExtensions[] = {".cpp", ".c", ".cc", ".cxx",
23                                         ".c++", ".m", ".mm"};
24   llvm::StringRef HeaderExtensions[] = {".h", ".hh", ".hpp", ".hxx", ".inc"};
25 
26   llvm::StringRef PathExt = llvm::sys::path::extension(OriginalFile);
27 
28   // Lookup in a list of known extensions.
29   auto SourceIter =
30       llvm::find_if(SourceExtensions, [&PathExt](PathRef SourceExt) {
31         return SourceExt.equals_lower(PathExt);
32       });
33   bool IsSource = SourceIter != std::end(SourceExtensions);
34 
35   auto HeaderIter =
36       llvm::find_if(HeaderExtensions, [&PathExt](PathRef HeaderExt) {
37         return HeaderExt.equals_lower(PathExt);
38       });
39   bool IsHeader = HeaderIter != std::end(HeaderExtensions);
40 
41   // We can only switch between the known extensions.
42   if (!IsSource && !IsHeader)
43     return None;
44 
45   // Array to lookup extensions for the switch. An opposite of where original
46   // extension was found.
47   llvm::ArrayRef<llvm::StringRef> NewExts;
48   if (IsSource)
49     NewExts = HeaderExtensions;
50   else
51     NewExts = SourceExtensions;
52 
53   // Storage for the new path.
54   llvm::SmallString<128> NewPath = llvm::StringRef(OriginalFile);
55 
56   // Loop through switched extension candidates.
57   for (llvm::StringRef NewExt : NewExts) {
58     llvm::sys::path::replace_extension(NewPath, NewExt);
59     if (VFS->exists(NewPath))
60       return NewPath.str().str(); // First str() to convert from SmallString to
61                                   // StringRef, second to convert from StringRef
62                                   // to std::string
63 
64     // Also check NewExt in upper-case, just in case.
65     llvm::sys::path::replace_extension(NewPath, NewExt.upper());
66     if (VFS->exists(NewPath))
67       return NewPath.str().str();
68   }
69   return None;
70 }
71 
getCorrespondingHeaderOrSource(const Path & OriginalFile,ParsedAST & AST,const SymbolIndex * Index)72 llvm::Optional<Path> getCorrespondingHeaderOrSource(const Path &OriginalFile,
73                                                     ParsedAST &AST,
74                                                     const SymbolIndex *Index) {
75   if (!Index) {
76     // FIXME: use the AST to do the inference.
77     return None;
78   }
79   LookupRequest Request;
80   // Find all symbols present in the original file.
81   for (const auto *D : getIndexableLocalDecls(AST)) {
82     if (auto ID = getSymbolID(D))
83       Request.IDs.insert(*ID);
84   }
85   llvm::StringMap<int> Candidates; // Target path => score.
86   auto AwardTarget = [&](const char *TargetURI) {
87     if (auto TargetPath = URI::resolve(TargetURI, OriginalFile)) {
88       if (*TargetPath != OriginalFile) // exclude the original file.
89         ++Candidates[*TargetPath];
90     } else {
91       elog("Failed to resolve URI {0}: {1}", TargetURI, TargetPath.takeError());
92     }
93   };
94   // If we switch from a header, we are looking for the implementation
95   // file, so we use the definition loc; otherwise we look for the header file,
96   // we use the decl loc;
97   //
98   // For each symbol in the original file, we get its target location (decl or
99   // def) from the index, then award that target file.
100   bool IsHeader = isHeaderFile(OriginalFile, AST.getLangOpts());
101   Index->lookup(Request, [&](const Symbol &Sym) {
102     if (IsHeader)
103       AwardTarget(Sym.Definition.FileURI);
104     else
105       AwardTarget(Sym.CanonicalDeclaration.FileURI);
106   });
107   // FIXME: our index doesn't have any interesting information (this could be
108   // that the background-index is not finished), we should use the decl/def
109   // locations from the AST to do the inference (from .cc to .h).
110   if (Candidates.empty())
111     return None;
112 
113   // Pickup the winner, who contains most of symbols.
114   // FIXME: should we use other signals (file proximity) to help score?
115   auto Best = Candidates.begin();
116   for (auto It = Candidates.begin(); It != Candidates.end(); ++It) {
117     if (It->second > Best->second)
118       Best = It;
119     else if (It->second == Best->second && It->first() < Best->first())
120       // Select the first one in the lexical order if we have multiple
121       // candidates.
122       Best = It;
123   }
124   return Path(std::string(Best->first()));
125 }
126 
getIndexableLocalDecls(ParsedAST & AST)127 std::vector<const Decl *> getIndexableLocalDecls(ParsedAST &AST) {
128   std::vector<const Decl *> Results;
129   std::function<void(Decl *)> TraverseDecl = [&](Decl *D) {
130     auto *ND = llvm::dyn_cast<NamedDecl>(D);
131     if (!ND || ND->isImplicit())
132       return;
133     if (!SymbolCollector::shouldCollectSymbol(*ND, D->getASTContext(), {},
134                                               /*IsMainFileSymbol=*/false))
135       return;
136     if (!llvm::isa<FunctionDecl>(ND)) {
137       // Visit the children, but we skip function decls as we are not interested
138       // in the function body.
139       if (auto *Scope = llvm::dyn_cast<DeclContext>(ND)) {
140         for (auto *D : Scope->decls())
141           TraverseDecl(D);
142       }
143     }
144     if (llvm::isa<NamespaceDecl>(D))
145       return; // namespace is indexable, but we're not interested.
146     Results.push_back(D);
147   };
148   // Traverses the ParsedAST directly to collect all decls present in the main
149   // file.
150   for (auto *TopLevel : AST.getLocalTopLevelDecls())
151     TraverseDecl(TopLevel);
152   return Results;
153 }
154 
155 } // namespace clangd
156 } // namespace clang
157