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