Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 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
201 changes: 200 additions & 1 deletion clang-tools-extra/clangd/FindSymbols.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,211 @@
#include "llvm/ADT/StringRef.h"
#include <limits>
#include <optional>
#include <tuple>

#define DEBUG_TYPE "FindSymbols"

namespace clang {
namespace clangd {

namespace {

// "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 toSymbolTagBitmask(const SymbolTag ST) {
return (1 << static_cast<unsigned>(ST));
}

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 +440,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
22 changes: 22 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,18 @@ getWorkspaceSymbols(llvm::StringRef Query, int Limit,
/// same order that they appear.
llvm::Expected<std::vector<DocumentSymbol>> getDocumentSymbols(ParsedAST &AST);

/// Converts a single SymbolTag to a bitmask.
SymbolTags toSymbolTagBitmask(SymbolTag ST);

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

/// Returns the symbol tags for the given declaration.
/// This is a wrapper around computeSymbolTags() which unpacks
/// the tags into a vector.
/// \p ND The declaration to get tags for.
std::vector<SymbolTag> getSymbolTags(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
56 changes: 44 additions & 12 deletions clang-tools-extra/clangd/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ struct TextDocumentEdit {
/// The text document to change.
VersionedTextDocumentIdentifier textDocument;

/// The edits to be applied.
/// The edits to be applied.
/// FIXME: support the AnnotatedTextEdit variant.
std::vector<TextEdit> edits;
};
Expand Down Expand Up @@ -560,7 +560,7 @@ struct ClientCapabilities {

/// The client supports versioned document changes for WorkspaceEdit.
bool DocumentChanges = false;

/// The client supports change annotations on text edits,
bool ChangeAnnotation = false;

Expand Down Expand Up @@ -1027,12 +1027,12 @@ struct WorkspaceEdit {
/// Versioned document edits.
///
/// If a client neither supports `documentChanges` nor
/// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s
/// using the `changes` property are supported.
/// `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s
/// using the `changes` property are supported.
std::optional<std::vector<TextDocumentEdit>> documentChanges;

/// A map of change annotations that can be referenced in
/// AnnotatedTextEdit.
/// AnnotatedTextEdit.
std::map<std::string, ChangeAnnotation> changeAnnotations;
};
bool fromJSON(const llvm::json::Value &, WorkspaceEdit &, llvm::json::Path);
Expand Down Expand Up @@ -1104,6 +1104,35 @@ struct CodeAction {
};
llvm::json::Value toJSON(const CodeAction &);

/// Symbol tags are extra annotations that can be attached to a symbol.
/// \see https://github.com/microsoft/language-server-protocol/pull/2003
enum class SymbolTag {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I'm understanding this correctly, the list and order here is coming from microsoft/language-server-protocol#2003.

Can we link to that PR from a comment here? That makes it clear that we're implementing something that's not in the spec yet, and serves as a reminder for us to update the implementation if the spec ends up being different from the current list in the PR.

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

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 not seeing the requested comment.

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

Deprecated = 1,
Private = 2,
Package = 3,
Protected = 4,
Public = 5,
Internal = 6,
File = 7,
Static = 8,
Abstract = 9,
Final = 10,
Sealed = 11,
Transient = 12,
Volatile = 13,
Synchronized = 14,
Virtual = 15,
Nullable = 16,
NonNull = 17,
Declaration = 18,
Definition = 19,
ReadOnly = 20,

// Update as needed
FirstTag = Deprecated,
LastTag = ReadOnly
};
llvm::json::Value toJSON(SymbolTag);
/// Represents programming constructs like variables, classes, interfaces etc.
/// that appear in a document. Document symbols can be hierarchical and they
/// have two ranges: one that encloses its definition and one that points to its
Expand All @@ -1121,6 +1150,9 @@ struct DocumentSymbol {
/// Indicates if this symbol is deprecated.
bool deprecated = false;

/// The tags for this symbol.
std::vector<SymbolTag> tags;

/// The range enclosing this symbol not including leading/trailing whitespace
/// but everything else like comments. This information is typically used to
/// determine if the clients cursor is inside the symbol to reveal in the
Expand All @@ -1146,6 +1178,9 @@ struct SymbolInformation {
/// The kind of this symbol.
SymbolKind kind;

/// Tags for this symbol, e.g public, private, static, const etc.
std::vector<SymbolTag> tags;

/// The location of this symbol.
Location location;

Expand Down Expand Up @@ -1288,13 +1323,13 @@ enum class InsertTextFormat {
/// Additional details for a completion item label.
struct CompletionItemLabelDetails {
/// An optional string which is rendered less prominently directly after label
/// without any spacing. Should be used for function signatures or type
/// without any spacing. Should be used for function signatures or type
/// annotations.
std::string detail;

/// An optional string which is rendered less prominently after
/// CompletionItemLabelDetails.detail. Should be used for fully qualified
/// names or file path.
/// CompletionItemLabelDetails.detail. Should be used for fully qualified
/// names or file path.
std::string description;
};
llvm::json::Value toJSON(const CompletionItemLabelDetails &);
Expand Down Expand Up @@ -1572,9 +1607,6 @@ struct ResolveTypeHierarchyItemParams {
bool fromJSON(const llvm::json::Value &, ResolveTypeHierarchyItemParams &,
llvm::json::Path);

enum class SymbolTag { Deprecated = 1 };
llvm::json::Value toJSON(SymbolTag);

/// The parameter of a `textDocument/prepareCallHierarchy` request.
struct CallHierarchyPrepareParams : public TextDocumentPositionParams {};

Expand Down
Loading