[clangd] Add support for additional symbol tags proposed for LSP 3.18 (#167536)

Implements support for symbol tags proposed for LSP 3.18 in
https://github.com/microsoft/language-server-protocol/pull/2003,
in the `documentSymbols` and `workspace/symbols` requests.

Fixes https://github.com/clangd/clangd/issues/2123.

---------

Co-authored-by: chouzz <zhouhua258@outlook.com>
Co-authored-by: Dimitri Ratz <dimitri.ratz@thinkdigital.cc>
This commit is contained in:
Dimitri Ratz 2026-01-23 04:00:15 +01:00 committed by GitHub
parent 700b5f1387
commit c961174b2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 472 additions and 149 deletions

View File

@ -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) {
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);
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 {
@ -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;

View File

@ -14,6 +14,7 @@
#include "Protocol.h"
#include "index/Symbol.h"
#include "clang/AST/Decl.h"
#include "llvm/ADT/StringRef.h"
namespace clang {
@ -21,6 +22,15 @@ 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;
/// 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);
@ -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

View File

@ -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);
}

View File

@ -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;
};
@ -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;
@ -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);
@ -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 {
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
@ -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
@ -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;
@ -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 &);
@ -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 {};

View File

@ -7,7 +7,9 @@
//===----------------------------------------------------------------------===//
#include "SemanticHighlighting.h"
#include "AST.h"
#include "Config.h"
#include "FindSymbols.h"
#include "FindTarget.h"
#include "ParsedAST.h"
#include "Protocol.h"
@ -21,9 +23,7 @@
#include "clang/AST/DeclarationName.h"
#include "clang/AST/ExprCXX.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/AST/Type.h"
#include "clang/AST/TypeLoc.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Sema/HeuristicResolver.h"
@ -32,6 +32,7 @@
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/Error.h"
#include <algorithm>
#include <optional>
@ -77,23 +78,6 @@ bool canHighlightName(DeclarationName Name) {
llvm_unreachable("invalid name kind");
}
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);
}
std::optional<HighlightingKind> kindForType(const Type *TP,
const HeuristicResolver *Resolver);
std::optional<HighlightingKind> kindForDecl(const NamedDecl *D,
@ -147,10 +131,9 @@ std::optional<HighlightingKind> kindForDecl(const NamedDecl *D,
if (auto *VD = dyn_cast<VarDecl>(D)) {
if (isa<ImplicitParamDecl>(VD)) // e.g. ObjC Self
return std::nullopt;
return VD->isStaticDataMember()
? HighlightingKind::StaticField
: VD->isLocalVarDecl() ? HighlightingKind::LocalVariable
: HighlightingKind::Variable;
return VD->isStaticDataMember() ? HighlightingKind::StaticField
: VD->isLocalVarDecl() ? HighlightingKind::LocalVariable
: HighlightingKind::Variable;
}
if (const auto *BD = dyn_cast<BindingDecl>(D))
return BD->getDeclContext()->isFunctionOrMethod()
@ -192,91 +175,6 @@ std::optional<HighlightingKind> kindForType(const Type *TP,
return std::nullopt;
}
// 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;
}
return false;
}
// "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) {
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();
return false;
}
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;
}
bool isVirtual(const Decl *D) {
if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(D))
return CMD->isVirtual();
return false;
}
bool isDependent(const Decl *D) {
if (isa<UnresolvedUsingValueDecl>(D))
return true;
@ -1157,31 +1055,41 @@ getSemanticHighlightings(ParsedAST &AST, bool IncludeInactiveRegionTokens) {
}
if (auto Mod = scopeModifier(Decl))
Tok.addModifier(*Mod);
if (isConst(Decl))
Tok.addModifier(HighlightingModifier::Readonly);
if (isStatic(Decl))
Tok.addModifier(HighlightingModifier::Static);
if (isAbstract(Decl))
Tok.addModifier(HighlightingModifier::Abstract);
if (isVirtual(Decl))
Tok.addModifier(HighlightingModifier::Virtual);
if (isDependent(Decl))
Tok.addModifier(HighlightingModifier::DependentName);
if (isDefaultLibrary(Decl))
Tok.addModifier(HighlightingModifier::DefaultLibrary);
if (Decl->isDeprecated())
Tok.addModifier(HighlightingModifier::Deprecated);
if (isa<CXXConstructorDecl>(Decl))
Tok.addModifier(HighlightingModifier::ConstructorOrDestructor);
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))
const auto SymbolTags = computeSymbolTags(*Decl);
static const thread_local llvm::DenseMap<SymbolTag,
HighlightingModifier>
TagModifierMap = {
{SymbolTag::Deprecated, HighlightingModifier::Deprecated},
{SymbolTag::ReadOnly, HighlightingModifier::Readonly},
{SymbolTag::Static, HighlightingModifier::Static},
{SymbolTag::Virtual, HighlightingModifier::Virtual},
{SymbolTag::Abstract, HighlightingModifier::Abstract},
// Declaration and Definition are handled separately below.
};
for (const auto &[Tag, Modifier] : TagModifierMap) {
if (SymbolTags & toSymbolTagBitmask(Tag))
Tok.addModifier(Modifier);
}
if (R.IsDecl &&
(SymbolTags & toSymbolTagBitmask(SymbolTag::Declaration))) {
Tok.addModifier(HighlightingModifier::Declaration);
if (SymbolTags & toSymbolTagBitmask(SymbolTag::Definition))
Tok.addModifier(HighlightingModifier::Definition);
}
if (isDependent(Decl))
Tok.addModifier(HighlightingModifier::DependentName);
if (isDefaultLibrary(Decl))
Tok.addModifier(HighlightingModifier::DefaultLibrary);
if (isa<CXXConstructorDecl>(Decl))
Tok.addModifier(HighlightingModifier::ConstructorOrDestructor);
}
},
AST.getHeuristicResolver());
@ -1501,9 +1409,8 @@ llvm::StringRef toSemanticTokenModifier(HighlightingModifier Modifier) {
llvm_unreachable("unhandled HighlightingModifier");
}
std::vector<SemanticTokensEdit>
diffTokens(llvm::ArrayRef<SemanticToken> Old,
llvm::ArrayRef<SemanticToken> New) {
std::vector<SemanticTokensEdit> diffTokens(llvm::ArrayRef<SemanticToken> Old,
llvm::ArrayRef<SemanticToken> New) {
// For now, just replace everything from the first-last modification.
// FIXME: use a real diff instead, this is bad with include-insertion.

View File

@ -0,0 +1,85 @@
# COM: Checks the extraction of symbol tags.
# RUN: clangd -lit-test < %s | FileCheck %s
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument":{"documentSymbol":{"hierarchicalDocumentSymbolSupport":true}},"workspace":{"symbol":{"symbolKind":{"valueSet": [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}}}},"trace":"off"}}
---
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,
"text":
"class A {\n virtual void f() const = 0;\n};"
}}}
---
{"jsonrpc":"2.0","id":2,"method":"textDocument/documentSymbol","params":{"textDocument":{"uri":"test:///main.cpp"}}}
# CHECK: "id": 2
# CHECK: "jsonrpc": "2.0",
# CHECK: "result": [
# CHECK: {
# CHECK: "children": [
# CHECK: {
# CHECK: "detail": "void () const",
# CHECK: "kind": 6,
# CHECK: "name": "f",
# CHECK: "range": {
# CHECK: "end": {
# CHECK: "character": 27,
# CHECK: "line": 1
# CHECK: },
# CHECK: "start": {
# CHECK: "character": 1,
# CHECK: "line": 1
# CHECK: }
# CHECK: },
# CHECK: "selectionRange": {
# CHECK: "end": {
# CHECK: "character": 15,
# CHECK: "line": 1
# CHECK: },
# CHECK: "start": {
# CHECK: "character": 14,
# CHECK: "line": 1
# CHECK: }
# CHECK: },
# CHECK: "tags": [
# CHECK: 2,
# CHECK: 9,
# CHECK: 15,
# CHECK: 18,
# CHECK: 20
# CHECK: ]
# CHECK: }
# CHECK: ],
# CHECK: "detail": "class",
# CHECK: "kind": 5,
# CHECK: "name": "A",
# CHECK: "range": {
# CHECK: "end": {
# CHECK: "character": 1,
# CHECK: "line": 2
# CHECK: },
# CHECK: "start": {
# CHECK: "character": 0,
# CHECK: "line": 0
# CHECK: }
# CHECK: },
# CHECK: "selectionRange": {
# CHECK: "end": {
# CHECK: "character": 7,
# CHECK: "line": 0
# CHECK: },
# CHECK: "start": {
# CHECK: "character": 6,
# CHECK: "line": 0
# CHECK: }
# CHECK: },
# CHECK: "tags": [
# CHECK: 9,
# CHECK: 18,
# CHECK: 19
# CHECK: ]
# CHECK: }
# CHECK: ]
# CHECK: }
---
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
---
{"jsonrpc":"2.0","method":"exit"}

View File

@ -56,7 +56,10 @@
# CHECK-NEXT: "character": {{.*}},
# CHECK-NEXT: "line": {{.*}}
# CHECK-NEXT: }
# CHECK-NEXT: }
# CHECK-NEXT: },
# CHECK-NEXT: "tags": [
# CHECK-NEXT: 18
# CHECK-NEXT: ]
# CHECK-NEXT: },
# CHECK-NEXT: {
# CHECK-NEXT: "detail": "int ()",
@ -81,7 +84,11 @@
# CHECK-NEXT: "character": {{.*}},
# CHECK-NEXT: "line": {{.*}}
# CHECK-NEXT: }
# CHECK-NEXT: }
# CHECK-NEXT: },
# CHECK-NEXT: "tags": [
# CHECK-NEXT: 18,
# CHECK-NEXT: 19
# CHECK-NEXT: ]
# CHECK-NEXT: }
# CHECK-NEXT: ]
# CHECK-NEXT:}

View File

@ -43,6 +43,12 @@ template <class... ChildMatchers>
return Field(&DocumentSymbol::children, UnorderedElementsAre(ChildrenM...));
}
template <typename... Tags>
::testing::Matcher<DocumentSymbol> withSymbolTags(Tags... tags) {
// Matches the tags vector ignoring element order.
return Field(&DocumentSymbol::tags, UnorderedElementsAre(tags...));
}
std::vector<SymbolInformation> getSymbols(TestTU &TU, llvm::StringRef Query,
int Limit = 0) {
auto SymbolInfos = getWorkspaceSymbols(Query, Limit, TU.index().get(),
@ -1132,6 +1138,69 @@ TEST(DocumentSymbolsTest, PragmaMarkGroupsNoNesting) {
withName("Core"), withName("coreMethod")));
}
TEST(DocumentSymbolsTest, SymbolTags) {
TestTU TU;
Annotations Main(R"cpp(
class AbstractClass {
public:
virtual ~AbstractClass() = default;
virtual void f1() = 0;
void f2() const;
protected:
void f3(){}
private:
static void f4(){}
};
void AbstractClass::f2() const {}
class ImplClass final: public AbstractClass {
public:
void f1() final {}
};
)cpp");
TU.Code = Main.code().str();
auto Symbols = getSymbols(TU.build());
EXPECT_THAT(
Symbols,
UnorderedElementsAre(
AllOf(
withName("AbstractClass"),
withSymbolTags(SymbolTag::Abstract, SymbolTag::Declaration,
SymbolTag::Definition),
children(
AllOf(withName("~AbstractClass"),
withSymbolTags(SymbolTag::Public, SymbolTag::Virtual,
SymbolTag::Declaration,
SymbolTag::Definition)),
AllOf(withName("f1"),
withSymbolTags(SymbolTag::Public, SymbolTag::Abstract,
SymbolTag::Virtual,
SymbolTag::Declaration)),
AllOf(withName("f2"), withSymbolTags(SymbolTag::Public,
SymbolTag::Declaration,
SymbolTag::ReadOnly)),
AllOf(withName("f3"), withSymbolTags(SymbolTag::Protected,
SymbolTag::Declaration,
SymbolTag::Definition)),
AllOf(withName("f4"),
withSymbolTags(SymbolTag::Private, SymbolTag::Static,
SymbolTag::Declaration,
SymbolTag::Definition)))),
AllOf(withName("AbstractClass::f2"),
withSymbolTags(SymbolTag::Public, SymbolTag::Declaration,
SymbolTag::Definition, SymbolTag::ReadOnly)),
AllOf(withName("ImplClass"),
withSymbolTags(SymbolTag::Final, SymbolTag::Declaration,
SymbolTag::Definition),
children(AllOf(
withName("f1"),
withSymbolTags(SymbolTag::Public, SymbolTag::Final,
SymbolTag::Virtual, SymbolTag::Declaration,
SymbolTag::Definition))))));
}
} // namespace
} // namespace clangd
} // namespace clang