Skip to content

C++: clangd uses single-header TU resolution #1373

Description

@pikaro

Summary

  • To resolve headers included from other headers, clangd requires a TU context
  • Without context, it falls back on bare minimum implementation and fails to discover symbols / discovers false truths
  • In header-only C++, this makes clangd almost useless without context
  • As a result, Serena is unable to provide agents with the required structural information

Reproduction

I use platformio with ESP IDF and its clangd as my only C++ toolchain, which makes it harder to provide a full reproduction. However a C++ programmer will be able to put this in context easily, and this is not a toolchain-dependent issue per se.

include/Outer/Interface/Foo.hpp

#pragma once
#include "Outer/detail/Concepts.hpp"

namespace Outer {
struct Foo {
    template <typename T>
        requires detail::is_foo_like<T>
    static void make(T &&obj) {
        obj.foo();
    }
};
} // namespace Outer

include/Outer/detail/Concepts.hpp

#pragma once
#include <concepts>

namespace Outer::detail {
template <typename T>
concept is_foo_like = requires(T x) {
    { x.foo() } -> std::same_as<void>;
};
} // namespace Outer::detail

src/main.cpp

#include "Outer/Interfaces/Foo.hpp"

struct X {
    void foo() {}
};

extern "C" void app_main(void);

void app_main() { Outer::Foo::make(X{}); }

Explanation

Foo.hpp requires Concepts.hpp, but even in an editor, main.cpp must be opened before clangd is able to relate the two headers through compile_commands.json. When opening only Foo.hpp, a hover call on make will resolve to the Foo struct itself instead of the actual method because the is_foo_like concept is unknown.

### struct `Foo`  

Size: 1 byte, alignment 1 byte  

// In namespace Outer
struct Foo {}

The #include statement reports a "no such file" error from clangd.

If main.cpp is opened first, the output becomes

### static-method `make`  

→ `void`  
Parameters:  
- `T && obj`

// In Foo
public: template <typename T>
    requires detail::is_foo_like<T>
static void make(T &&obj)

The same happens in Serena: Project indexing opens the Foo.hpp header in isolation, and serena project index returns

ERROR 2026-04-18 14:53:59,626 solidlsp.ls_process:_read_ls_process_stderr:391 - [active_file=include/Outer/Interfaces/Foo.hpp] E[14:53:59.625] IncludeCleaner: Failed to get an entry for resolved path '' from include "Outer/detail/Concepts.hpp" : No such file or directory

Codex 5.4 behavior

Prompt:

Please use Serena MCP to resolve references to the make() method on the Foo struct. Do not fall back to text search either directly or via Serena; use ONLY LSP capabilties.

• Called serena.find_symbol({"name_path_pattern":"Foo/make","relative_path":"include/Outer/Interfaces/Foo.hpp","include_body":true,"max_matches":1})
  └ []

• Called serena.get_symbols_overview({"relative_path":"include/Outer/Interfaces/Foo.hpp","depth":1,"max_answer_chars":4000})
  └ {"Namespace": [{"Outer": {"Class": ["Foo"]}}], "File": ["Outer/detail/Concepts.hpp"]}

The initial direct symbol lookup didn’t match the method path [...]
One caveat: Serena’s index exposed Outer::Foo cleanly, but not make as a separately addressable symbol in this template/constrained method form [...]

CCodex also separately confirmed that this is exactly what happens when it tries to mirror how Serena is using clangd to build the symbols database - without TU context, many symbols fall back to something else or are otherwise not usable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions