Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
103b18f
[clangd] Support symbolTags for document symbol
chouzz Oct 25, 2024
18c71ac
Fix access for class method
chouzz Oct 28, 2024
af6491c
Support Declaration and Definition tags
chouzz Oct 31, 2024
baa29b1
Remove constant tag
chouzz Nov 18, 2024
64d5a36
Fix test
Nov 11, 2025
49551b5
Tagging final methods and classes
Nov 12, 2025
8a4b314
Checks the extraction of symbol tags
Nov 13, 2025
f29267e
Merge branch 'main' into users/ratzdi/symbol_tags
ratzdi Nov 13, 2025
49574d8
Docu update
Nov 13, 2025
a5a8555
Fix code format.
Nov 18, 2025
979f7e7
Merge branch 'main' into users/ratzdi/symbol_tags
ratzdi Nov 19, 2025
b52210f
Merge branch 'main' into users/ratzdi/symbol_tags
ratzdi Nov 19, 2025
8131e6f
Fill symbol tags for the response CallHierarchyItem of prepareCallHie…
Nov 27, 2025
317eb1d
Fix call-hierarchy.test
Nov 28, 2025
d899c71
Merge branch 'main' into users/ratzdi/symbol_tags
ratzdi Dec 3, 2025
1b39d9c
Merge branch 'main' into users/ratzdi/symbol_tags
ratzdi Dec 16, 2025
c0c6506
Merge branch 'llvm:main' into users/ratzdi/symbol_tags
ratzdi Jan 5, 2026
346fa11
Merge remote-tracking branch 'fork/users/ratzdi/symbol_tags' into use…
Jan 5, 2026
4117ba1
Reduced AST API, added unit-test introduced symbol-tag bitmask.
Jan 9, 2026
e181082
Use parts of result of the function computeSymbolTags in SemanticHigh…
Jan 12, 2026
b7a2003
Merge branch 'llvm:main' into users/ratzdi/symbol_tags
ratzdi Jan 12, 2026
6f0deff
Overload computeSymbolTags to encapsulate the proper determination of…
Jan 14, 2026
ea5fd1c
Relocated computeSymbolTags to the module FindSymbols.
Jan 14, 2026
438b02f
Fixed tests.
Jan 14, 2026
53a8c8f
Includes clean-up.
Jan 14, 2026
31dc8a5
Removed computeSymbolTags(const NamedDecl &ND) from public API.
Jan 14, 2026
04144e5
Fix format.
Jan 15, 2026
64978c3
Review findings:
Jan 15, 2026
b265a9c
Fix test.
Jan 15, 2026
bd233a4
Review findings:
Jan 19, 2026
2eb7d3b
Review findings:
Jan 20, 2026
3333550
Fix format.
Jan 20, 2026
49a4a78
Review findings:
Jan 21, 2026
1ae72f4
Merge branch 'main' into users/ratzdi/symbol_tags
ratzdi Jan 21, 2026
585210a
Restored include.
Jan 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions clang-tools-extra/clangd/AST.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "AST.h"

#include "SemanticHighlighting.h"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This include is not necessary any more.

I would suggest removing the changes to AST.cpp and call-hierarchy.test from this diff altogether.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

#include "SourceCode.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTTypeTraits.h"
Expand Down Expand Up @@ -617,6 +618,7 @@ class DeducedTypeVisitor : public RecursiveASTVisitor<DeducedTypeVisitor> {

QualType DeducedType;
};

} // namespace

std::optional<QualType> getDeducedType(ASTContext &ASTCtx,
Expand Down
200 changes: 199 additions & 1 deletion clang-tools-extra/clangd/FindSymbols.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,210 @@
#include "llvm/ADT/StringRef.h"
#include <limits>
#include <optional>
#include <tuple>

#define DEBUG_TYPE "FindSymbols"

namespace clang {
namespace clangd {

namespace {
SymbolTags toSymbolTagBitmask(const SymbolTag ST) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to expose this function in FindSymbols.h, given that SemanticHighlighting.cpp also uses it (and more generally, that it's needed to make practical use of the result of computeSymbolTags())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return (1 << static_cast<unsigned>(ST));
}

// "Static" means many things in C++, only some get the "static" modifier.
//
// Meanings that do:
// - Members associated with the class rather than the instance.
// This is what 'static' most often means across languages.
// - static local variables
// These are similarly "detached from their context" by the static keyword.
// In practice, these are rarely used inside classes, reducing confusion.
//
// Meanings that don't:
// - Namespace-scoped variables, which have static storage class.
// This is implicit, so the keyword "static" isn't so strongly associated.
// If we want a modifier for these, "global scope" is probably the concept.
// - Namespace-scoped variables/functions explicitly marked "static".
// There the keyword changes *linkage* , which is a totally different concept.
// If we want to model this, "file scope" would be a nice modifier.
//
// This is confusing, and maybe we should use another name, but because "static"
// is a standard LSP modifier, having one with that name has advantages.
bool isStatic(const Decl *D) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put the helpers (i.e. anything not exposed in the header) in the anonymous namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
return CMD->isStatic();
if (const VarDecl *VD = llvm::dyn_cast<VarDecl>(D))
return VD->isStaticDataMember() || VD->isStaticLocal();
if (const auto *OPD = llvm::dyn_cast<ObjCPropertyDecl>(D))
return OPD->isClassProperty();
if (const auto *OMD = llvm::dyn_cast<ObjCMethodDecl>(D))
return OMD->isClassMethod();
if (const auto *FD = llvm::dyn_cast<FunctionDecl>(D))
return FD->isStatic();
return false;
}

// Whether T is const in a loose sense - is a variable with this type readonly?
bool isConst(QualType T) {
if (T.isNull())
return false;
T = T.getNonReferenceType();
if (T.isConstQualified())
return true;
if (const auto *AT = T->getAsArrayTypeUnsafe())
return isConst(AT->getElementType());
if (isConst(T->getPointeeType()))
return true;
return false;
}

// Whether D is const in a loose sense (should it be highlighted as such?)
// FIXME: This is separate from whether *a particular usage* can mutate D.
// We may want V in V.size() to be readonly even if V is mutable.
bool isConst(const Decl *D) {
if (llvm::isa<EnumConstantDecl>(D) || llvm::isa<NonTypeTemplateParmDecl>(D))
return true;
if (llvm::isa<FieldDecl>(D) || llvm::isa<VarDecl>(D) ||
llvm::isa<MSPropertyDecl>(D) || llvm::isa<BindingDecl>(D)) {
if (isConst(llvm::cast<ValueDecl>(D)->getType()))
return true;
}
if (const auto *OCPD = llvm::dyn_cast<ObjCPropertyDecl>(D)) {
if (OCPD->isReadOnly())
return true;
}
if (const auto *MPD = llvm::dyn_cast<MSPropertyDecl>(D)) {
if (!MPD->hasSetter())
return true;
}
if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D)) {
if (CMD->isConst())
return true;
}
if (const auto *FD = llvm::dyn_cast<FunctionDecl>(D))
return isConst(FD->getReturnType());
return false;
}

// Indicates whether declaration D is abstract in cases where D is a struct or a
// class.
bool isAbstract(const Decl *D) {
if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
return CMD->isPureVirtual();
if (const auto *CRD = llvm::dyn_cast<CXXRecordDecl>(D))
return CRD->hasDefinition() && CRD->isAbstract();
return false;
}

// Indicates whether declaration D is virtual in cases where D is a method.
bool isVirtual(const Decl *D) {
if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
return CMD->isVirtual();
return false;
}

// Indicates whether declaration D is final in cases where D is a struct, class
// or method.
bool isFinal(const Decl *D) {
if (const auto *CRD = dyn_cast<CXXMethodDecl>(D))
return CRD->hasAttr<FinalAttr>();

if (const auto *CRD = dyn_cast<CXXRecordDecl>(D))
return CRD->hasAttr<FinalAttr>();

return false;
}

// Indicates whether declaration D is a unique definition (as opposed to a
// declaration).
bool isUniqueDefinition(const NamedDecl *Decl) {
if (auto *Func = dyn_cast<FunctionDecl>(Decl))
return Func->isThisDeclarationADefinition();
if (auto *Klass = dyn_cast<CXXRecordDecl>(Decl))
return Klass->isThisDeclarationADefinition();
if (auto *Iface = dyn_cast<ObjCInterfaceDecl>(Decl))
return Iface->isThisDeclarationADefinition();
if (auto *Proto = dyn_cast<ObjCProtocolDecl>(Decl))
return Proto->isThisDeclarationADefinition();
if (auto *Var = dyn_cast<VarDecl>(Decl))
return Var->isThisDeclarationADefinition();
return isa<TemplateTypeParmDecl>(Decl) ||
isa<NonTypeTemplateParmDecl>(Decl) ||
isa<TemplateTemplateParmDecl>(Decl) || isa<ObjCCategoryDecl>(Decl) ||
isa<ObjCImplDecl>(Decl);
}
} // namespace

SymbolTags computeSymbolTags(const NamedDecl &ND) {
SymbolTags Result = 0;
const auto IsDef = isUniqueDefinition(&ND);

if (ND.isDeprecated())
Result |= toSymbolTagBitmask(SymbolTag::Deprecated);

if (isConst(&ND))
Result |= toSymbolTagBitmask(SymbolTag::ReadOnly);

if (isStatic(&ND))
Result |= toSymbolTagBitmask(SymbolTag::Static);

if (isVirtual(&ND))
Result |= toSymbolTagBitmask(SymbolTag::Virtual);

if (isAbstract(&ND))
Result |= toSymbolTagBitmask(SymbolTag::Abstract);

if (isFinal(&ND))
Result |= toSymbolTagBitmask(SymbolTag::Final);

if (not isa<UnresolvedUsingValueDecl>(ND)) {
// Do not treat an UnresolvedUsingValueDecl as a declaration.
// It's more common to think of it as a reference to the
// underlying declaration.
Result |= toSymbolTagBitmask(SymbolTag::Declaration);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit unclear on the intended semantics of the Declaration tag, especially as the FindSymbols call site is passing IsDecl = true.

Is the intention that every document symbol is a Declaration (except UnresolvedUsingValueDecl which is a minor edge case)?

The wording in the PR -- specifically "Render a symbol as definition (in contrast to declaration)" -- makes me think perhaps Declaration should only be set for things that are not a Definition?

(I have some ideas about cleaning up the API to avoid the IsDecl parameter, but we first need to understand what the behaviour we're trying to implement is.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In SemanticHighlighting.cpp (main branch Line 1132) there is an if-statement that I have kept (otherwise breaking tests) in my work (without precise knowledge of the purpose of the condition)

if (R.IsDecl) {
  // Do not treat an UnresolvedUsingValueDecl as a declaration.
  // It's more common to think of it as a reference to the
  // underlying declaration.
  if (!isa<UnresolvedUsingValueDecl>(Decl))
    Tok.addModifier(HighlightingModifier::Declaration);
  if (isUniqueDefinition(Decl))
    Tok.addModifier(HighlightingModifier::Definition);
}

now the if-statement is located in computeSymbolTags
const auto SymbolTags = computeSymbolTags(*Decl, R.IsDecl);

and the lines

...

const auto SymbolTags = computeSymbolTags(*Decl, R.IsDecl);

...

if (SymbolTags & toSymbolTagBitmask(SymbolTag::Declaration))
  Tok.addModifier(HighlightingModifier::Declaration);

if (SymbolTags & toSymbolTagBitmask(SymbolTag::Definition))
  Tok.addModifier(HighlightingModifier::Definition);

...

replacing the code block, mentioned above.

The wording in the PR -- specifically "Render a symbol as definition (in contrast to declaration)" -- makes me think perhaps Declaration should only be set for things that are not a Definition?

When we decide to either ... or, some tests will break.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording in the PR -- specifically "Render a symbol as definition (in contrast to declaration)" -- makes me think perhaps Declaration should only be set for things that are not a Definition?

When we decide to either ... or, some tests will break.

I'm assuming you're referring to semantic highlighting tests.

I'm talking here about the expected behaviour of textDocument/documentSymbol.

Say you're a client like Eclipse, and you send a textDocument/documentSymbol request, e.g. for the purpose of populating an Outline view. Are there any cases (ignoring the edge case of UnresolvedUsingValueDecl) where you'd want a returned symbol not to have the Declaration symbol tag set?

Copy link
Contributor Author

@ratzdi ratzdi Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no such cases.

The code block below arises from the assumption that a definition is always also a declaration. A definition cannot exist without a declaration. I now think that my previous assumption is incorrect, especially in this context.

  if (IsDecl && not isa<UnresolvedUsingValueDecl>(ND)) {
    // Do not treat an UnresolvedUsingValueDecl as a declaration.
    // It's more common to think of it as a reference to the
    // underlying declaration.
    Result |= toSymbolTagBitmask(SymbolTag::Declaration);

    if (IsDef)
      Result |= toSymbolTagBitmask(SymbolTag::Definition);
  }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no such cases.

Ok.

(This makes me question a bit why the Declaration modifier exists at all -- surely the authors of the LSP proposal weren't thinking of using-declarations -- but perhaps that's something we can follow up on in microsoft/language-server-protocol#2003.)

So, in terms of implementation strategy, I would suggest the following:

  • Remove the IsDecl parameter from computeSymbolTags() (keep the implementation as if it was true)
  • In SemanticHighlighting.cpp, do:
    if (R.IsDecl && (SymbolTags & toSymbolTagBitmask(SymbolTag::Declaration)))
      Tok.addModifier(HighlightingModifier::Declaration);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (IsDef)
Result |= toSymbolTagBitmask(SymbolTag::Definition);
}

switch (ND.getAccess()) {
case AS_public:
Result |= toSymbolTagBitmask(SymbolTag::Public);
break;
case AS_protected:
Result |= toSymbolTagBitmask(SymbolTag::Protected);
break;
case AS_private:
Result |= toSymbolTagBitmask(SymbolTag::Private);
break;
default:
break;
}

return Result;
}

std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND) {
const auto symbolTags = computeSymbolTags(ND);
std::vector<SymbolTag> Tags;

if (symbolTags == 0)
return Tags;

// Iterate through SymbolTag enum values and collect any that are present in
// the bitmask. SymbolTag values are in the numeric range
// [FirstTag .. LastTag].
constexpr unsigned MinTag = static_cast<unsigned>(SymbolTag::FirstTag);
constexpr unsigned MaxTag = static_cast<unsigned>(SymbolTag::LastTag);
for (unsigned I = MinTag; I <= MaxTag; ++I) {
auto ST = static_cast<SymbolTag>(I);
if (symbolTags & toSymbolTagBitmask(ST))
Tags.push_back(ST);
}
return Tags;
}

namespace {
using ScoredSymbolInfo = std::pair<float, SymbolInformation>;
struct ScoredSymbolGreater {
Expand Down Expand Up @@ -242,6 +439,7 @@ std::optional<DocumentSymbol> declToSym(ASTContext &Ctx, const NamedDecl &ND) {
SI.range = Range{sourceLocToPosition(SM, SymbolRange->getBegin()),
sourceLocToPosition(SM, SymbolRange->getEnd())};
SI.detail = getSymbolDetail(Ctx, ND);
SI.tags = getSymbolTags(ND);

SourceLocation NameLoc = ND.getLocation();
SourceLocation FallbackNameLoc;
Expand Down
17 changes: 17 additions & 0 deletions clang-tools-extra/clangd/FindSymbols.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@

#include "Protocol.h"
#include "index/Symbol.h"
#include "clang/AST/Decl.h"
#include "llvm/ADT/StringRef.h"

namespace clang {
namespace clangd {
class ParsedAST;
class SymbolIndex;

/// A bitmask type representing symbol tags supported by LSP.
/// \see
/// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#symbolTag
using SymbolTags = uint32_t;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's static_assert(SymbolTag::LastTag < 32). If the number of tags ever grows beyond that, it will serve as a reminder to change this to uint64_t.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// Ensure we have enough bits to represent all SymbolTag values.
static_assert(static_cast<unsigned>(SymbolTag::LastTag) <= 32,
"Too many SymbolTags to fit in uint32_t. Change to uint64_t if "
"we ever have more than 32 tags.");

/// Helper function for deriving an LSP Location from an index SymbolLocation.
llvm::Expected<Location> indexToLSPLocation(const SymbolLocation &Loc,
llvm::StringRef TUPath);
Expand All @@ -47,6 +57,13 @@ getWorkspaceSymbols(llvm::StringRef Query, int Limit,
/// same order that they appear.
llvm::Expected<std::vector<DocumentSymbol>> getDocumentSymbols(ParsedAST &AST);

/// Returns the symbol tags for the given declaration.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add:

/// This is a wrapper around computeSymbolTags() which unpacks
/// the tags into a vector.

and move the declaration below computeSymbolTags().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// \p ND The declaration to get tags for.
std::vector<SymbolTag> getSymbolTags(const NamedDecl &ND);

/// Computes symbol tags for a given NamedDecl.
SymbolTags computeSymbolTags(const NamedDecl &ND);

} // namespace clangd
} // namespace clang

Expand Down
2 changes: 2 additions & 0 deletions clang-tools-extra/clangd/Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,8 @@ llvm::json::Value toJSON(const DocumentSymbol &S) {
Result["children"] = S.children;
if (S.deprecated)
Result["deprecated"] = true;
if (!S.tags.empty())
Result["tags"] = S.tags;
// FIXME: workaround for older gcc/clang
return std::move(Result);
}
Expand Down
Loading