-
Notifications
You must be signed in to change notification settings - Fork 15.3k
Description
When using clangd, the auto-include suggestions are broken in a project like the following (which you can clone from https://git.jade.fyi/jade/clangd-bug-repro):
build.sh:
clang++ -std=c++20 -Iinclude/ src/foo.cc -c -o src/foo.cc.o
Generate a compile_commands.json via bear or so.
Tree:
.
├── build.sh
├── compile_commands.json
├── include
│ └── lib2 -> ../lib2
├── lib2
│ ├── bar.hh
│ └── foo.hh
└── src
├── foo.cc
├── nya.hh
└── wat.hh
foo.cc:
#include "lib2/foo.hh"
#include "nya.hh"
#include "wat.hh"
void thingy() {
frob();
wat();
lolwut();
}lib2/foo.hh:
int frob();
lib2/bar.hh:
void showNyanCat();
Then open lib2/bar.hh in a new tab to make sure clangd knows about it. Tab-complete a call to showNyanCat(), which will not add the header include. By comparison, doing the same for a symbol in, e.g., wat.hh, will successfully add the include.
I have debugged this to find that HeaderPath::suggestPathToFileForDiagnostics returns an absolute path for the file in question, which is then promptly rejected downstream by clangd:
llvm-project/clang-tools-extra/clangd/Headers.cpp
Lines 292 to 306 in 3f156ef
| if (HeaderSearchInfo) { | |
| Suggested = HeaderSearchInfo->suggestPathToFileForDiagnostics( | |
| InsertedHeader.File, BuildDir, IncludingFile, &IsAngled); | |
| } else { | |
| // Calculate include relative to including file only. | |
| StringRef IncludingDir = llvm::sys::path::parent_path(IncludingFile); | |
| SmallString<256> RelFile(InsertedHeader.File); | |
| // Replacing with "" leaves "/RelFile" if IncludingDir doesn't end in "/". | |
| llvm::sys::path::replace_path_prefix(RelFile, IncludingDir, "./"); | |
| Suggested = llvm::sys::path::convert_to_slash( | |
| llvm::sys::path::remove_leading_dotslash(RelFile)); | |
| } | |
| // FIXME: should we allow (some limited number of) "../header.h"? | |
| if (llvm::sys::path::is_absolute(Suggested)) | |
| return std::nullopt; |
The proximate cause of the absolute paths from suggestPathToFileForDiagnostics is that there are no -I directories or parents of the file in question that are common prefixes of the canonical file path of the translation unit and the canonical path of the header file. Seemingly the only way to get this to work today is to make the canonical path of the header a child of the include directory. However, this requires rearranging source code around what is IMO a clang bug or at least deficiency.
Another alternative (which I have not tried, and which has kind of bad ergonomics since it is busted for newly created files) is generating a header map specifically for clangd since those are special cased in suggestPathToFileForDiagnostics. Yet another alternative which does work but is absurd is:
.clangd containing {"CompileFlags": {"Add": ["-I", ".include"], "Remove": ["-I", "../src/include"]}} and then running clangd in a bwrap that bindmounts .include/lix = src (in the actual problem in the field, the problem is that we have includes of the form lix/libfoo/bar.hh which need to resolve to src/libfoo/bar.hh, so just putting more ../ is not a fix).
I have tried the following VFS overlay:
version: 0
roots:
- name: include/lib2
type: directory-remap
external-contents: lib2/
# plus or minus the following line; same results either way
# use-external-name: trueand then added via -Xclang -ivfsoverlay -Xclang ./vfsoverlay.yaml. This compiles fine but the include inserter still refuses to work for presumably the same reason. It appears that the remapping is treated as if it is a symlink rather than as a bind mount, I think. I have not shoved the thing into rr yet.
At this point I've spent enough time debugging and writing this up that I am looking for input on whether this is a completely busted use case or if I have missed something in this adventure.