Skip to content

[clangd] Support go-to-definition on type hints. The core part #86629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 43 commits into
base: main
Choose a base branch
from

Conversation

zyn0217
Copy link
Contributor

@zyn0217 zyn0217 commented Mar 26, 2024

This patch is going to offer go-to-definition support on nested type template arguments, for example:

vvvvvvvvv    The primary template of Container
Container<llvm::SmallVector<TemplateArgument>, std::vector<bool>>
                ^^^^^^^^^^^                                      links to the primary template of `llvm::SmallVector`

                           ^^^^^^^^^^^^^^^^                     links to the definition of `clang::TemplateArgument`

                                                   ^^^^^^       links to the template specialization of `std::vector<bool>`

the clickable links on template names don't involve their namespace specifiers, e.g. the llvm:: part in llvm::SmallVector will not be combined together with the following SmallVector links. (This is just because I'm too lazy to implement the combination logic, not because we want to distinguish something. The fun fact is, which I've also checked, that CLion doesn't even offer namespaces/type specifiers at all for inlay type hints.)

Things we probably want besides this patch:

  • A configuration entry for disabling type links if the current strategy causes crashes/spammy results or if the user just doesn't want it.
  • Some corner cases overlooked by this patch (e.g. attributes/C++20 structural NTTP types)

Resolves clangd/clangd#1535

@zyn0217 zyn0217 requested a review from HighCommander4 March 26, 2024 05:43
Copy link

github-actions bot commented Mar 26, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@zyn0217 zyn0217 force-pushed the inlay-hint-label-part-impl branch from c8f1495 to fa5fb4a Compare March 31, 2024 15:14
@zyn0217
Copy link
Contributor Author

zyn0217 commented Apr 5, 2024

I think I'm ready for feedback after around one week of dogfooding myself.

@zyn0217 zyn0217 marked this pull request as ready for review April 5, 2024 03:16
@llvmbot
Copy link
Member

llvmbot commented Apr 5, 2024

@llvm/pr-subscribers-clang-tools-extra

Author: Younan Zhang (zyn0217)

Changes

This patch is going to offer go-to-definition support on nested type template arguments, for example:

     vvv no links, as we can almost always follow the template name using the corresponding `auto` keyword
std::map&lt;llvm::SmallVector&lt;TemplateArgument&gt;, std::vector&lt;bool&gt;&gt;
               ^^^^^^^^^^^                                      links to the primary template of `llvm::SmallVector`

                           ^^^^^^^^^^^^^^^^                     links to the definition of `clang::TemplateArgument`

                                                   ^^^^^^       links to the template specialization of `std::vector&lt;bool&gt;`

the clickable links on template names don't involve their namespace specifiers, e.g. the llvm:: part in llvm::SmallVector will not be combined together with the following SmallVector links. (This is just because I'm too lazy to implement the combination logic, not because we want to distinguish something. The fun fact is, which I've also checked, that CLion don't even offer namespaces/type specifiers at all for inlay type hints.)

Things we probably want besides this patch:

  • A configuration entry for disabling type links if the current strategy causes crashes/spammy results or if the user just doesn't want it.
  • Some corner cases overlooked by this patch (e.g. attributes/C++20 structural NTTP types)

Resolves clangd/clangd#1535


Patch is 23.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/86629.diff

5 Files Affected:

  • (modified) clang-tools-extra/clangd/AST.cpp (+9)
  • (modified) clang-tools-extra/clangd/AST.h (+2)
  • (modified) clang-tools-extra/clangd/InlayHints.cpp (+332-9)
  • (modified) clang-tools-extra/clangd/index/IndexAction.cpp (+3-6)
  • (modified) clang-tools-extra/clangd/unittests/InlayHintTests.cpp (+160)
diff --git a/clang-tools-extra/clangd/AST.cpp b/clang-tools-extra/clangd/AST.cpp
index 1b86ea19cf28da..ef87f1bcb8443c 100644
--- a/clang-tools-extra/clangd/AST.cpp
+++ b/clang-tools-extra/clangd/AST.cpp
@@ -1019,5 +1019,14 @@ bool isExpandedFromParameterPack(const ParmVarDecl *D) {
   return getUnderlyingPackType(D) != nullptr;
 }
 
+std::optional<URI> toURI(OptionalFileEntryRef File) {
+  if (!File)
+    return std::nullopt;
+  auto AbsolutePath = File->getFileEntry().tryGetRealPathName();
+  if (AbsolutePath.empty())
+    return std::nullopt;
+  return URI::create(AbsolutePath);
+}
+
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/AST.h b/clang-tools-extra/clangd/AST.h
index fb0722d697cd06..3ae624b1ab7415 100644
--- a/clang-tools-extra/clangd/AST.h
+++ b/clang-tools-extra/clangd/AST.h
@@ -250,6 +250,8 @@ resolveForwardingParameters(const FunctionDecl *D, unsigned MaxDepth = 10);
 /// reference to one (e.g. `Args&...` or `Args&&...`).
 bool isExpandedFromParameterPack(const ParmVarDecl *D);
 
+std::optional<URI> toURI(OptionalFileEntryRef File);
+
 } // namespace clangd
 } // namespace clang
 
diff --git a/clang-tools-extra/clangd/InlayHints.cpp b/clang-tools-extra/clangd/InlayHints.cpp
index cd4f1931b3ce1d..8d8fbc4bcde042 100644
--- a/clang-tools-extra/clangd/InlayHints.cpp
+++ b/clang-tools-extra/clangd/InlayHints.cpp
@@ -11,6 +11,7 @@
 #include "Config.h"
 #include "HeuristicResolver.h"
 #include "ParsedAST.h"
+#include "Protocol.h"
 #include "SourceCode.h"
 #include "clang/AST/ASTDiagnostic.h"
 #include "clang/AST/Decl.h"
@@ -21,6 +22,7 @@
 #include "clang/AST/Stmt.h"
 #include "clang/AST/StmtVisitor.h"
 #include "clang/AST/Type.h"
+#include "clang/AST/TypeVisitor.h"
 #include "clang/Basic/Builtins.h"
 #include "clang/Basic/OperatorKinds.h"
 #include "clang/Basic/SourceManager.h"
@@ -372,6 +374,276 @@ maybeDropCxxExplicitObjectParameters(ArrayRef<const ParmVarDecl *> Params) {
   return Params;
 }
 
+std::optional<Location> toLocation(SourceManager &SM, SourceRange Range) {
+  if (Range.isInvalid())
+    return std::nullopt;
+  if (auto URI =
+          toURI(SM.getFileEntryRefForID(SM.getFileID(Range.getBegin())))) {
+    Location L;
+    L.range.start = sourceLocToPosition(SM, Range.getBegin());
+    L.range.end = sourceLocToPosition(SM, Range.getEnd());
+    if (auto File = URIForFile::fromURI(*URI, ""))
+      L.uri = File.get();
+    return L;
+  }
+  return std::nullopt;
+}
+
+class TypeInlayHintLabelPartBuilder
+    : public TypeVisitor<TypeInlayHintLabelPartBuilder> {
+  QualType CurrentType;
+  ASTContext &Context;
+  const PrintingPolicy &PP;
+  std::vector<InlayHintLabelPart> &LabelChunks;
+
+  bool ShouldAddLinksToTagTypes;
+
+  struct CurrentTypeRAII {
+    TypeInlayHintLabelPartBuilder &Builder;
+    QualType PreviousType;
+    bool PreviousShouldAddLinksToTagTypes;
+    CurrentTypeRAII(TypeInlayHintLabelPartBuilder &Builder, QualType New,
+                    bool ShouldAddLinksToTagTypes)
+        : Builder(Builder), PreviousType(Builder.CurrentType),
+          PreviousShouldAddLinksToTagTypes(Builder.ShouldAddLinksToTagTypes) {
+      Builder.CurrentType = New;
+      Builder.ShouldAddLinksToTagTypes = ShouldAddLinksToTagTypes;
+    }
+    ~CurrentTypeRAII() {
+      Builder.CurrentType = PreviousType;
+      Builder.ShouldAddLinksToTagTypes = PreviousShouldAddLinksToTagTypes;
+    }
+  };
+
+  void addLabel(llvm::function_ref<void(llvm::raw_ostream &)> NamePrinter,
+                llvm::function_ref<SourceRange()> SourceRangeGetter) {
+    std::string Label;
+    llvm::raw_string_ostream OS(Label);
+    NamePrinter(OS);
+    if (!ShouldAddLinksToTagTypes)
+      return addLabel(std::move(Label));
+    auto &Name = LabelChunks.emplace_back();
+    Name.value = std::move(Label);
+    Name.location = toLocation(Context.getSourceManager(), SourceRangeGetter());
+  }
+
+  void addLabel(std::string Label) {
+    if (LabelChunks.empty()) {
+      LabelChunks.emplace_back(std::move(Label));
+      return;
+    }
+    auto &Back = LabelChunks.back();
+    if (Back.location) {
+      LabelChunks.emplace_back(std::move(Label));
+      return;
+    }
+    // Let's combine the "unclickable" pieces together.
+    Back.value += std::move(Label);
+  }
+
+  void printTemplateArgumentList(llvm::ArrayRef<TemplateArgument> Args) {
+    unsigned Size = Args.size();
+    for (unsigned I = 0; I < Size; ++I) {
+      auto &TA = Args[I];
+      if (PP.SuppressDefaultTemplateArgs && TA.getIsDefaulted())
+        continue;
+      if (I)
+        addLabel(", ");
+      printTemplateArgument(TA);
+    }
+  }
+
+  void printTemplateArgument(const TemplateArgument &TA) {
+    switch (TA.getKind()) {
+    case TemplateArgument::Pack:
+      return printTemplateArgumentList(TA.pack_elements());
+    case TemplateArgument::Type: {
+      CurrentTypeRAII Guard(*this, TA.getAsType(),
+                            /*ShouldAddLinksToTagTypes=*/true);
+      return Visit(TA.getAsType().getTypePtr());
+    }
+    // TODO: Add support for NTTP arguments.
+    case TemplateArgument::Expression:
+    case TemplateArgument::StructuralValue:
+    case TemplateArgument::Null:
+    case TemplateArgument::Declaration:
+    case TemplateArgument::NullPtr:
+    case TemplateArgument::Integral:
+    case TemplateArgument::Template:
+    case TemplateArgument::TemplateExpansion:
+      break;
+    }
+    std::string Label;
+    llvm::raw_string_ostream OS(Label);
+    TA.print(PP, OS, /*IncludeType=*/true);
+    addLabel(std::move(Label));
+  }
+
+  void
+  handleTemplateSpecialization(TemplateName TN,
+                               llvm::ArrayRef<TemplateArgument> Args,
+                               SourceRange TemplateNameRange = SourceRange()) {
+    SourceRange Range;
+    TemplateDecl *TD = nullptr;
+    switch (TN.getKind()) {
+    case TemplateName::Template:
+      TD = TN.getAsTemplateDecl();
+      Range = TD->getSourceRange();
+      LLVM_FALLTHROUGH;
+    default:
+      break;
+    }
+
+    addLabel([&](llvm::raw_ostream &OS) { TN.print(OS, PP); },
+             [&] {
+               if (TemplateNameRange.isValid())
+                 return TemplateNameRange;
+               return Range;
+             });
+
+    addLabel("<");
+    printTemplateArgumentList(Args);
+    addLabel(">");
+  }
+
+  void maybeAddQualifiers() {
+    auto Quals = CurrentType.split().Quals;
+    if (!Quals.empty()) {
+      addLabel(Quals.getAsString());
+      addLabel(" ");
+    }
+  }
+
+  // When printing a reference, the referenced type might also be a reference.
+  // If so, we want to skip that before printing the inner type.
+  static QualType skipTopLevelReferences(QualType T) {
+    if (auto *Ref = T->getAs<ReferenceType>())
+      return skipTopLevelReferences(Ref->getPointeeTypeAsWritten());
+    return T;
+  }
+
+public:
+  TypeInlayHintLabelPartBuilder(QualType Current, ASTContext &Context,
+                                const PrintingPolicy &PP,
+                                bool ShouldAddLinksToTagTypes,
+                                llvm::StringRef Prefix,
+                                std::vector<InlayHintLabelPart> &LabelChunks)
+      : CurrentType(Current), Context(Context), PP(PP),
+        LabelChunks(LabelChunks),
+        ShouldAddLinksToTagTypes(ShouldAddLinksToTagTypes) {
+    LabelChunks.reserve(16);
+    if (!Prefix.empty())
+      addLabel(Prefix.str());
+  }
+
+  void VisitType(const Type *) { addLabel(CurrentType.getAsString(PP)); }
+
+  void VisitTagType(const TagType *TT) {
+    if (!ShouldAddLinksToTagTypes)
+      return VisitType(TT);
+    auto *D = TT->getDecl();
+    if (auto *Specialization = dyn_cast<ClassTemplateSpecializationDecl>(D))
+      return handleTemplateSpecialization(
+          TemplateName(Specialization->getSpecializedTemplate()),
+          Specialization->getTemplateArgs().asArray());
+    if (auto *RD = dyn_cast<CXXRecordDecl>(D);
+        RD && !RD->getTemplateInstantiationPattern())
+      return addLabel(
+          [&](llvm::raw_ostream &OS) { return RD->printName(OS, PP); },
+          [&] { return RD->getSourceRange(); });
+    return VisitType(TT);
+  }
+
+  void VisitAutoType(const AutoType *AT) {
+    if (!AT->isDeduced() || AT->getDeducedType()->isDecltypeType())
+      return;
+    maybeAddQualifiers();
+    CurrentTypeRAII Guard(*this, AT->getDeducedType(),
+                          ShouldAddLinksToTagTypes);
+    return Visit(AT->getDeducedType().getTypePtr());
+  }
+
+  void VisitElaboratedType(const ElaboratedType *ET) {
+    maybeAddQualifiers();
+    if (auto *NNS = ET->getQualifier()) {
+      switch (NNS->getKind()) {
+      case NestedNameSpecifier::Identifier:
+      case NestedNameSpecifier::Namespace:
+      case NestedNameSpecifier::NamespaceAlias:
+      case NestedNameSpecifier::Global:
+      case NestedNameSpecifier::Super: {
+        std::string Label;
+        llvm::raw_string_ostream OS(Label);
+        addLabel(std::move(Label));
+      } break;
+      case NestedNameSpecifier::TypeSpec:
+      case NestedNameSpecifier::TypeSpecWithTemplate:
+        CurrentTypeRAII Guard(
+            *this,
+            QualType(
+                NNS->getAsType(),
+                /*Quals=*/0), // Do we need cv-qualifiers on type specifiers?
+            ShouldAddLinksToTagTypes);
+        Visit(NNS->getAsType());
+        addLabel("::");
+        break;
+      }
+    }
+    CurrentTypeRAII Guard(*this, ET->getNamedType(), ShouldAddLinksToTagTypes);
+    return Visit(ET->getNamedType().getTypePtr());
+  }
+
+  void VisitReferenceType(const ReferenceType *RT) {
+    maybeAddQualifiers();
+    QualType Next = skipTopLevelReferences(RT->getPointeeTypeAsWritten());
+    CurrentTypeRAII Guard(*this, Next, ShouldAddLinksToTagTypes);
+    Visit(Next.getTypePtr());
+    if (Next->getPointeeType().isNull())
+      addLabel(" ");
+    if (RT->isLValueReferenceType())
+      addLabel("&");
+    if (RT->isRValueReferenceType())
+      addLabel("&&");
+  }
+
+  void VisitPointerType(const PointerType *PT) {
+    QualType Next = PT->getPointeeType();
+    std::optional<CurrentTypeRAII> Guard(std::in_place, *this, Next,
+                                         ShouldAddLinksToTagTypes);
+    Visit(Next.getTypePtr());
+    if (Next->getPointeeType().isNull())
+      addLabel(" ");
+    addLabel("*");
+    Guard.reset();
+    maybeAddQualifiers();
+  }
+
+  void VisitTemplateSpecializationType(const TemplateSpecializationType *TST) {
+    maybeAddQualifiers();
+    SourceRange Range;
+    if (auto *Specialization =
+            dyn_cast_if_present<ClassTemplateSpecializationDecl>(
+                TST->desugar().getCanonicalType()->getAsCXXRecordDecl()))
+      Range = Specialization->getSourceRange();
+    return handleTemplateSpecialization(TST->getTemplateName(),
+                                        TST->template_arguments(), Range);
+  }
+
+  void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType *ST) {
+    maybeAddQualifiers();
+    CurrentTypeRAII Guard(*this, ST->getReplacementType(),
+                          /*ShouldAddLinksToTagTypes=*/true);
+    return Visit(ST->getReplacementType().getTypePtr());
+  }
+};
+
+unsigned lengthOfInlayHintLabelPart(llvm::ArrayRef<InlayHintLabelPart> Labels) {
+  unsigned Size = 0;
+  for (auto &P : Labels)
+    Size += P.value.size();
+  return Size;
+}
+
 struct Callee {
   // Only one of Decl or Loc is set.
   // Loc is for calls through function pointers.
@@ -949,9 +1221,30 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
     addInlayHint(*LSPRange, Side, Kind, Prefix, Label, Suffix);
   }
 
+  void addInlayHint(SourceRange R, HintSide Side, InlayHintKind Kind,
+                    llvm::StringRef Prefix,
+                    std::vector<InlayHintLabelPart> Labels,
+                    llvm::StringRef Suffix) {
+    auto LSPRange = getHintRange(R);
+    if (!LSPRange)
+      return;
+
+    addInlayHint(*LSPRange, Side, Kind, Prefix, std::move(Labels), Suffix);
+  }
+
   void addInlayHint(Range LSPRange, HintSide Side, InlayHintKind Kind,
                     llvm::StringRef Prefix, llvm::StringRef Label,
                     llvm::StringRef Suffix) {
+    return addInlayHint(LSPRange, Side, Kind, Prefix,
+                        /*Labels=*/std::vector<InlayHintLabelPart>{Label.str()},
+                        Suffix);
+  }
+
+  void addInlayHint(Range LSPRange, HintSide Side, InlayHintKind Kind,
+                    llvm::StringRef Prefix,
+                    std::vector<InlayHintLabelPart> Labels,
+                    llvm::StringRef Suffix) {
+    assert(!Labels.empty() && "Expected non-empty labels");
     // We shouldn't get as far as adding a hint if the category is disabled.
     // We'd like to disable as much of the analysis as possible above instead.
     // Assert in debug mode but add a dynamic check in production.
@@ -977,9 +1270,27 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
       return;
     bool PadLeft = Prefix.consume_front(" ");
     bool PadRight = Suffix.consume_back(" ");
+    if (Prefix.empty() && Suffix.empty()) {
+      Results.push_back(InlayHint{LSPPos,
+                                  /*label=*/Labels, Kind, PadLeft, PadRight,
+                                  LSPRange});
+      return;
+    }
+    if (!Prefix.empty()) {
+      if (auto &Label = Labels.front(); !Label.location)
+        Label.value = Prefix.str() + Label.value;
+      else
+        Labels.insert(Labels.begin(), InlayHintLabelPart(Prefix.str()));
+    }
+    if (!Suffix.empty()) {
+      if (auto &Label = Labels.back(); !Label.location)
+        Label.value += Suffix.str();
+      else
+        Labels.push_back(InlayHintLabelPart(Suffix.str()));
+    }
     Results.push_back(InlayHint{LSPPos,
-                                /*label=*/{(Prefix + Label + Suffix).str()},
-                                Kind, PadLeft, PadRight, LSPRange});
+                                /*label=*/std::move(Labels), Kind, PadLeft,
+                                PadRight, LSPRange});
   }
 
   // Get the range of the main file that *exactly* corresponds to R.
@@ -1005,14 +1316,24 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
     // The sugared type is more useful in some cases, and the canonical
     // type in other cases.
     auto Desugared = maybeDesugar(AST, T);
-    std::string TypeName = Desugared.getAsString(TypeHintPolicy);
-    if (T != Desugared && !shouldPrintTypeHint(TypeName)) {
+    std::vector<InlayHintLabelPart> Chunks;
+    TypeInlayHintLabelPartBuilder Builder(
+        Desugared, AST, TypeHintPolicy,
+        /*ShouldAddLinksToTagTypes=*/T != Desugared, Prefix, Chunks);
+    Builder.Visit(Desugared.getTypePtr());
+    if (T != Desugared && !shouldPrintTypeHint(Chunks)) {
       // If the desugared type is too long to display, fallback to the sugared
       // type.
-      TypeName = T.getAsString(TypeHintPolicy);
+      Chunks.clear();
+      TypeInlayHintLabelPartBuilder Builder(T, AST, TypeHintPolicy,
+                                            /*ShouldAddLinksToTagTypes=*/false,
+                                            Prefix, Chunks);
+      Builder.Visit(T.getTypePtr());
     }
-    if (shouldPrintTypeHint(TypeName))
-      addInlayHint(R, HintSide::Right, InlayHintKind::Type, Prefix, TypeName,
+    if (shouldPrintTypeHint(Chunks))
+      addInlayHint(R, HintSide::Right, InlayHintKind::Type,
+                   /*Prefix=*/"", // We have handled prefixes in the builder.
+                   std::move(Chunks),
                    /*Suffix=*/"");
   }
 
@@ -1021,9 +1342,11 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
                  /*Prefix=*/"", Text, /*Suffix=*/"=");
   }
 
-  bool shouldPrintTypeHint(llvm::StringRef TypeName) const noexcept {
+  bool shouldPrintTypeHint(
+      llvm::ArrayRef<InlayHintLabelPart> TypeLabels) const noexcept {
     return Cfg.InlayHints.TypeNameLimit == 0 ||
-           TypeName.size() < Cfg.InlayHints.TypeNameLimit;
+           lengthOfInlayHintLabelPart(TypeLabels) <
+               Cfg.InlayHints.TypeNameLimit;
   }
 
   void addBlockEndHint(SourceRange BraceRange, StringRef DeclPrefix,
diff --git a/clang-tools-extra/clangd/index/IndexAction.cpp b/clang-tools-extra/clangd/index/IndexAction.cpp
index ed56c2a9d2e811..f3fbb9fc307e2e 100644
--- a/clang-tools-extra/clangd/index/IndexAction.cpp
+++ b/clang-tools-extra/clangd/index/IndexAction.cpp
@@ -32,12 +32,9 @@ namespace clangd {
 namespace {
 
 std::optional<std::string> toURI(OptionalFileEntryRef File) {
-  if (!File)
-    return std::nullopt;
-  auto AbsolutePath = File->getFileEntry().tryGetRealPathName();
-  if (AbsolutePath.empty())
-    return std::nullopt;
-  return URI::create(AbsolutePath).toString();
+  if (auto URI = clang::clangd::toURI(File))
+    return URI->toString();
+  return std::nullopt;
 }
 
 // Collects the nodes and edges of include graph during indexing action.
diff --git a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
index 5b1531eb2fa60b..a69422d825f30b 100644
--- a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
+++ b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
@@ -55,6 +55,19 @@ struct ExpectedHint {
   }
 };
 
+struct ExpectedHintLabelPiece {
+  std::string Label;
+  std::optional<std::string> PointsTo = std::nullopt;
+
+  friend llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream,
+                                       const ExpectedHintLabelPiece &Hint) {
+    Stream << Hint.Label;
+    if (Hint.PointsTo)
+      Stream << " that points to $" << Hint.PointsTo;
+    return Stream;
+  }
+};
+
 MATCHER_P2(HintMatcher, Expected, Code, llvm::to_string(Expected)) {
   llvm::StringRef ExpectedView(Expected.Label);
   std::string ResultLabel = arg.joinLabels();
@@ -73,6 +86,34 @@ MATCHER_P2(HintMatcher, Expected, Code, llvm::to_string(Expected)) {
   return true;
 }
 
+MATCHER_P2(HintLabelPieceMatcher, Expected, Code, llvm::to_string(Expected)) {
+  llvm::StringRef ExpectedView(Expected.Label);
+  std::string ResultLabel = arg.value;
+  if (ResultLabel != ExpectedView) {
+    *result_listener << "label is '" << ResultLabel << "'";
+    return false;
+  }
+  if (!Expected.PointsTo && !arg.location)
+    return true;
+  if (Expected.PointsTo && !arg.location) {
+    *result_listener << " range " << *Expected.PointsTo
+                     << " is expected, but we have nothing.";
+    return false;
+  }
+  if (!Expected.PointsTo && arg.location) {
+    *result_listener << " the link points to " << llvm::to_string(arg.location)
+                     << ", but we expect nothing.";
+    return false;
+  }
+  if (arg.location->range != Code.range(*Expected.PointsTo)) {
+    *result_listener << "range is " << llvm::to_string(arg.location->range)
+                     << " but $" << *Expected.PointsTo << " points to "
+                     << llvm::to_string(Code.range(*Expected.PointsTo));
+    return false;
+  }
+  return true;
+}
+
 MATCHER_P(labelIs, Label, "") { return arg.joinLabels() == Label; }
 
 Config noHintsConfig() {
@@ -1637,6 +1678,125 @@ TEST(TypeHints, SubstTemplateParameterAliases) {
                         ExpectedHint{": static_vector<int>", "vector_name"});
 }
 
+template <typename... Labels>
+void assertTypeLinkHints(StringRef Code, StringRef HintRange,
+                         Labels... ExpectedLabels) {
+  Annotations Source(Code);
+  auto HintAt = [&](llvm::ArrayRef<InlayHint> InlayHints,
+                    llvm::StringRef Range) {
+    auto *Hint = llvm::find_if(InlayHints, [&](const InlayHint &InlayHint) {
+      return InlayHint.range == Source.range(Range);
+    });
+    assert(Hint && "No range was found");
+    return llvm::ArrayRef(Hint->label);
+  };
+
+  TestTU TU = TestTU::withCode(Source.code());
+  TU.ExtraArgs.push_back("-std=c++2c");
+  auto AST = TU.build();
+
+  Config C;
+  C.InlayHints.TypeNameLimit = 0;
+  WithContextValue WithCfg(Config::Key, std::move(C));
+
+  auto Hints = hintsOfKind(AST, InlayHintKind::Type);
+  EXPECT_THAT(HintAt(Hints, HintRange),
+              ElementsAre(HintLabelPieceMatcher(ExpectedLabels, Source)...));
+}
+
+TEST(TypeHints, Links) {
+  StringRef Source(R"cpp(
+    $Package[[template <class T, class U>
+    struct Package {]]};
+
+    $SpecializationOfPackage[[template ...
[truncated]

@llvmbot
Copy link
Member

llvmbot commented Apr 5, 2024

@llvm/pr-subscribers-clangd

Author: Younan Zhang (zyn0217)

Changes

This patch is going to offer go-to-definition support on nested type template arguments, for example:

     vvv no links, as we can almost always follow the template name using the corresponding `auto` keyword
std::map&lt;llvm::SmallVector&lt;TemplateArgument&gt;, std::vector&lt;bool&gt;&gt;
               ^^^^^^^^^^^                                      links to the primary template of `llvm::SmallVector`

                           ^^^^^^^^^^^^^^^^                     links to the definition of `clang::TemplateArgument`

                                                   ^^^^^^       links to the template specialization of `std::vector&lt;bool&gt;`

the clickable links on template names don't involve their namespace specifiers, e.g. the llvm:: part in llvm::SmallVector will not be combined together with the following SmallVector links. (This is just because I'm too lazy to implement the combination logic, not because we want to distinguish something. The fun fact is, which I've also checked, that CLion don't even offer namespaces/type specifiers at all for inlay type hints.)

Things we probably want besides this patch:

  • A configuration entry for disabling type links if the current strategy causes crashes/spammy results or if the user just doesn't want it.
  • Some corner cases overlooked by this patch (e.g. attributes/C++20 structural NTTP types)

Resolves clangd/clangd#1535


Patch is 23.50 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/86629.diff

5 Files Affected:

  • (modified) clang-tools-extra/clangd/AST.cpp (+9)
  • (modified) clang-tools-extra/clangd/AST.h (+2)
  • (modified) clang-tools-extra/clangd/InlayHints.cpp (+332-9)
  • (modified) clang-tools-extra/clangd/index/IndexAction.cpp (+3-6)
  • (modified) clang-tools-extra/clangd/unittests/InlayHintTests.cpp (+160)
diff --git a/clang-tools-extra/clangd/AST.cpp b/clang-tools-extra/clangd/AST.cpp
index 1b86ea19cf28da..ef87f1bcb8443c 100644
--- a/clang-tools-extra/clangd/AST.cpp
+++ b/clang-tools-extra/clangd/AST.cpp
@@ -1019,5 +1019,14 @@ bool isExpandedFromParameterPack(const ParmVarDecl *D) {
   return getUnderlyingPackType(D) != nullptr;
 }
 
+std::optional<URI> toURI(OptionalFileEntryRef File) {
+  if (!File)
+    return std::nullopt;
+  auto AbsolutePath = File->getFileEntry().tryGetRealPathName();
+  if (AbsolutePath.empty())
+    return std::nullopt;
+  return URI::create(AbsolutePath);
+}
+
 } // namespace clangd
 } // namespace clang
diff --git a/clang-tools-extra/clangd/AST.h b/clang-tools-extra/clangd/AST.h
index fb0722d697cd06..3ae624b1ab7415 100644
--- a/clang-tools-extra/clangd/AST.h
+++ b/clang-tools-extra/clangd/AST.h
@@ -250,6 +250,8 @@ resolveForwardingParameters(const FunctionDecl *D, unsigned MaxDepth = 10);
 /// reference to one (e.g. `Args&...` or `Args&&...`).
 bool isExpandedFromParameterPack(const ParmVarDecl *D);
 
+std::optional<URI> toURI(OptionalFileEntryRef File);
+
 } // namespace clangd
 } // namespace clang
 
diff --git a/clang-tools-extra/clangd/InlayHints.cpp b/clang-tools-extra/clangd/InlayHints.cpp
index cd4f1931b3ce1d..8d8fbc4bcde042 100644
--- a/clang-tools-extra/clangd/InlayHints.cpp
+++ b/clang-tools-extra/clangd/InlayHints.cpp
@@ -11,6 +11,7 @@
 #include "Config.h"
 #include "HeuristicResolver.h"
 #include "ParsedAST.h"
+#include "Protocol.h"
 #include "SourceCode.h"
 #include "clang/AST/ASTDiagnostic.h"
 #include "clang/AST/Decl.h"
@@ -21,6 +22,7 @@
 #include "clang/AST/Stmt.h"
 #include "clang/AST/StmtVisitor.h"
 #include "clang/AST/Type.h"
+#include "clang/AST/TypeVisitor.h"
 #include "clang/Basic/Builtins.h"
 #include "clang/Basic/OperatorKinds.h"
 #include "clang/Basic/SourceManager.h"
@@ -372,6 +374,276 @@ maybeDropCxxExplicitObjectParameters(ArrayRef<const ParmVarDecl *> Params) {
   return Params;
 }
 
+std::optional<Location> toLocation(SourceManager &SM, SourceRange Range) {
+  if (Range.isInvalid())
+    return std::nullopt;
+  if (auto URI =
+          toURI(SM.getFileEntryRefForID(SM.getFileID(Range.getBegin())))) {
+    Location L;
+    L.range.start = sourceLocToPosition(SM, Range.getBegin());
+    L.range.end = sourceLocToPosition(SM, Range.getEnd());
+    if (auto File = URIForFile::fromURI(*URI, ""))
+      L.uri = File.get();
+    return L;
+  }
+  return std::nullopt;
+}
+
+class TypeInlayHintLabelPartBuilder
+    : public TypeVisitor<TypeInlayHintLabelPartBuilder> {
+  QualType CurrentType;
+  ASTContext &Context;
+  const PrintingPolicy &PP;
+  std::vector<InlayHintLabelPart> &LabelChunks;
+
+  bool ShouldAddLinksToTagTypes;
+
+  struct CurrentTypeRAII {
+    TypeInlayHintLabelPartBuilder &Builder;
+    QualType PreviousType;
+    bool PreviousShouldAddLinksToTagTypes;
+    CurrentTypeRAII(TypeInlayHintLabelPartBuilder &Builder, QualType New,
+                    bool ShouldAddLinksToTagTypes)
+        : Builder(Builder), PreviousType(Builder.CurrentType),
+          PreviousShouldAddLinksToTagTypes(Builder.ShouldAddLinksToTagTypes) {
+      Builder.CurrentType = New;
+      Builder.ShouldAddLinksToTagTypes = ShouldAddLinksToTagTypes;
+    }
+    ~CurrentTypeRAII() {
+      Builder.CurrentType = PreviousType;
+      Builder.ShouldAddLinksToTagTypes = PreviousShouldAddLinksToTagTypes;
+    }
+  };
+
+  void addLabel(llvm::function_ref<void(llvm::raw_ostream &)> NamePrinter,
+                llvm::function_ref<SourceRange()> SourceRangeGetter) {
+    std::string Label;
+    llvm::raw_string_ostream OS(Label);
+    NamePrinter(OS);
+    if (!ShouldAddLinksToTagTypes)
+      return addLabel(std::move(Label));
+    auto &Name = LabelChunks.emplace_back();
+    Name.value = std::move(Label);
+    Name.location = toLocation(Context.getSourceManager(), SourceRangeGetter());
+  }
+
+  void addLabel(std::string Label) {
+    if (LabelChunks.empty()) {
+      LabelChunks.emplace_back(std::move(Label));
+      return;
+    }
+    auto &Back = LabelChunks.back();
+    if (Back.location) {
+      LabelChunks.emplace_back(std::move(Label));
+      return;
+    }
+    // Let's combine the "unclickable" pieces together.
+    Back.value += std::move(Label);
+  }
+
+  void printTemplateArgumentList(llvm::ArrayRef<TemplateArgument> Args) {
+    unsigned Size = Args.size();
+    for (unsigned I = 0; I < Size; ++I) {
+      auto &TA = Args[I];
+      if (PP.SuppressDefaultTemplateArgs && TA.getIsDefaulted())
+        continue;
+      if (I)
+        addLabel(", ");
+      printTemplateArgument(TA);
+    }
+  }
+
+  void printTemplateArgument(const TemplateArgument &TA) {
+    switch (TA.getKind()) {
+    case TemplateArgument::Pack:
+      return printTemplateArgumentList(TA.pack_elements());
+    case TemplateArgument::Type: {
+      CurrentTypeRAII Guard(*this, TA.getAsType(),
+                            /*ShouldAddLinksToTagTypes=*/true);
+      return Visit(TA.getAsType().getTypePtr());
+    }
+    // TODO: Add support for NTTP arguments.
+    case TemplateArgument::Expression:
+    case TemplateArgument::StructuralValue:
+    case TemplateArgument::Null:
+    case TemplateArgument::Declaration:
+    case TemplateArgument::NullPtr:
+    case TemplateArgument::Integral:
+    case TemplateArgument::Template:
+    case TemplateArgument::TemplateExpansion:
+      break;
+    }
+    std::string Label;
+    llvm::raw_string_ostream OS(Label);
+    TA.print(PP, OS, /*IncludeType=*/true);
+    addLabel(std::move(Label));
+  }
+
+  void
+  handleTemplateSpecialization(TemplateName TN,
+                               llvm::ArrayRef<TemplateArgument> Args,
+                               SourceRange TemplateNameRange = SourceRange()) {
+    SourceRange Range;
+    TemplateDecl *TD = nullptr;
+    switch (TN.getKind()) {
+    case TemplateName::Template:
+      TD = TN.getAsTemplateDecl();
+      Range = TD->getSourceRange();
+      LLVM_FALLTHROUGH;
+    default:
+      break;
+    }
+
+    addLabel([&](llvm::raw_ostream &OS) { TN.print(OS, PP); },
+             [&] {
+               if (TemplateNameRange.isValid())
+                 return TemplateNameRange;
+               return Range;
+             });
+
+    addLabel("<");
+    printTemplateArgumentList(Args);
+    addLabel(">");
+  }
+
+  void maybeAddQualifiers() {
+    auto Quals = CurrentType.split().Quals;
+    if (!Quals.empty()) {
+      addLabel(Quals.getAsString());
+      addLabel(" ");
+    }
+  }
+
+  // When printing a reference, the referenced type might also be a reference.
+  // If so, we want to skip that before printing the inner type.
+  static QualType skipTopLevelReferences(QualType T) {
+    if (auto *Ref = T->getAs<ReferenceType>())
+      return skipTopLevelReferences(Ref->getPointeeTypeAsWritten());
+    return T;
+  }
+
+public:
+  TypeInlayHintLabelPartBuilder(QualType Current, ASTContext &Context,
+                                const PrintingPolicy &PP,
+                                bool ShouldAddLinksToTagTypes,
+                                llvm::StringRef Prefix,
+                                std::vector<InlayHintLabelPart> &LabelChunks)
+      : CurrentType(Current), Context(Context), PP(PP),
+        LabelChunks(LabelChunks),
+        ShouldAddLinksToTagTypes(ShouldAddLinksToTagTypes) {
+    LabelChunks.reserve(16);
+    if (!Prefix.empty())
+      addLabel(Prefix.str());
+  }
+
+  void VisitType(const Type *) { addLabel(CurrentType.getAsString(PP)); }
+
+  void VisitTagType(const TagType *TT) {
+    if (!ShouldAddLinksToTagTypes)
+      return VisitType(TT);
+    auto *D = TT->getDecl();
+    if (auto *Specialization = dyn_cast<ClassTemplateSpecializationDecl>(D))
+      return handleTemplateSpecialization(
+          TemplateName(Specialization->getSpecializedTemplate()),
+          Specialization->getTemplateArgs().asArray());
+    if (auto *RD = dyn_cast<CXXRecordDecl>(D);
+        RD && !RD->getTemplateInstantiationPattern())
+      return addLabel(
+          [&](llvm::raw_ostream &OS) { return RD->printName(OS, PP); },
+          [&] { return RD->getSourceRange(); });
+    return VisitType(TT);
+  }
+
+  void VisitAutoType(const AutoType *AT) {
+    if (!AT->isDeduced() || AT->getDeducedType()->isDecltypeType())
+      return;
+    maybeAddQualifiers();
+    CurrentTypeRAII Guard(*this, AT->getDeducedType(),
+                          ShouldAddLinksToTagTypes);
+    return Visit(AT->getDeducedType().getTypePtr());
+  }
+
+  void VisitElaboratedType(const ElaboratedType *ET) {
+    maybeAddQualifiers();
+    if (auto *NNS = ET->getQualifier()) {
+      switch (NNS->getKind()) {
+      case NestedNameSpecifier::Identifier:
+      case NestedNameSpecifier::Namespace:
+      case NestedNameSpecifier::NamespaceAlias:
+      case NestedNameSpecifier::Global:
+      case NestedNameSpecifier::Super: {
+        std::string Label;
+        llvm::raw_string_ostream OS(Label);
+        addLabel(std::move(Label));
+      } break;
+      case NestedNameSpecifier::TypeSpec:
+      case NestedNameSpecifier::TypeSpecWithTemplate:
+        CurrentTypeRAII Guard(
+            *this,
+            QualType(
+                NNS->getAsType(),
+                /*Quals=*/0), // Do we need cv-qualifiers on type specifiers?
+            ShouldAddLinksToTagTypes);
+        Visit(NNS->getAsType());
+        addLabel("::");
+        break;
+      }
+    }
+    CurrentTypeRAII Guard(*this, ET->getNamedType(), ShouldAddLinksToTagTypes);
+    return Visit(ET->getNamedType().getTypePtr());
+  }
+
+  void VisitReferenceType(const ReferenceType *RT) {
+    maybeAddQualifiers();
+    QualType Next = skipTopLevelReferences(RT->getPointeeTypeAsWritten());
+    CurrentTypeRAII Guard(*this, Next, ShouldAddLinksToTagTypes);
+    Visit(Next.getTypePtr());
+    if (Next->getPointeeType().isNull())
+      addLabel(" ");
+    if (RT->isLValueReferenceType())
+      addLabel("&");
+    if (RT->isRValueReferenceType())
+      addLabel("&&");
+  }
+
+  void VisitPointerType(const PointerType *PT) {
+    QualType Next = PT->getPointeeType();
+    std::optional<CurrentTypeRAII> Guard(std::in_place, *this, Next,
+                                         ShouldAddLinksToTagTypes);
+    Visit(Next.getTypePtr());
+    if (Next->getPointeeType().isNull())
+      addLabel(" ");
+    addLabel("*");
+    Guard.reset();
+    maybeAddQualifiers();
+  }
+
+  void VisitTemplateSpecializationType(const TemplateSpecializationType *TST) {
+    maybeAddQualifiers();
+    SourceRange Range;
+    if (auto *Specialization =
+            dyn_cast_if_present<ClassTemplateSpecializationDecl>(
+                TST->desugar().getCanonicalType()->getAsCXXRecordDecl()))
+      Range = Specialization->getSourceRange();
+    return handleTemplateSpecialization(TST->getTemplateName(),
+                                        TST->template_arguments(), Range);
+  }
+
+  void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType *ST) {
+    maybeAddQualifiers();
+    CurrentTypeRAII Guard(*this, ST->getReplacementType(),
+                          /*ShouldAddLinksToTagTypes=*/true);
+    return Visit(ST->getReplacementType().getTypePtr());
+  }
+};
+
+unsigned lengthOfInlayHintLabelPart(llvm::ArrayRef<InlayHintLabelPart> Labels) {
+  unsigned Size = 0;
+  for (auto &P : Labels)
+    Size += P.value.size();
+  return Size;
+}
+
 struct Callee {
   // Only one of Decl or Loc is set.
   // Loc is for calls through function pointers.
@@ -949,9 +1221,30 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
     addInlayHint(*LSPRange, Side, Kind, Prefix, Label, Suffix);
   }
 
+  void addInlayHint(SourceRange R, HintSide Side, InlayHintKind Kind,
+                    llvm::StringRef Prefix,
+                    std::vector<InlayHintLabelPart> Labels,
+                    llvm::StringRef Suffix) {
+    auto LSPRange = getHintRange(R);
+    if (!LSPRange)
+      return;
+
+    addInlayHint(*LSPRange, Side, Kind, Prefix, std::move(Labels), Suffix);
+  }
+
   void addInlayHint(Range LSPRange, HintSide Side, InlayHintKind Kind,
                     llvm::StringRef Prefix, llvm::StringRef Label,
                     llvm::StringRef Suffix) {
+    return addInlayHint(LSPRange, Side, Kind, Prefix,
+                        /*Labels=*/std::vector<InlayHintLabelPart>{Label.str()},
+                        Suffix);
+  }
+
+  void addInlayHint(Range LSPRange, HintSide Side, InlayHintKind Kind,
+                    llvm::StringRef Prefix,
+                    std::vector<InlayHintLabelPart> Labels,
+                    llvm::StringRef Suffix) {
+    assert(!Labels.empty() && "Expected non-empty labels");
     // We shouldn't get as far as adding a hint if the category is disabled.
     // We'd like to disable as much of the analysis as possible above instead.
     // Assert in debug mode but add a dynamic check in production.
@@ -977,9 +1270,27 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
       return;
     bool PadLeft = Prefix.consume_front(" ");
     bool PadRight = Suffix.consume_back(" ");
+    if (Prefix.empty() && Suffix.empty()) {
+      Results.push_back(InlayHint{LSPPos,
+                                  /*label=*/Labels, Kind, PadLeft, PadRight,
+                                  LSPRange});
+      return;
+    }
+    if (!Prefix.empty()) {
+      if (auto &Label = Labels.front(); !Label.location)
+        Label.value = Prefix.str() + Label.value;
+      else
+        Labels.insert(Labels.begin(), InlayHintLabelPart(Prefix.str()));
+    }
+    if (!Suffix.empty()) {
+      if (auto &Label = Labels.back(); !Label.location)
+        Label.value += Suffix.str();
+      else
+        Labels.push_back(InlayHintLabelPart(Suffix.str()));
+    }
     Results.push_back(InlayHint{LSPPos,
-                                /*label=*/{(Prefix + Label + Suffix).str()},
-                                Kind, PadLeft, PadRight, LSPRange});
+                                /*label=*/std::move(Labels), Kind, PadLeft,
+                                PadRight, LSPRange});
   }
 
   // Get the range of the main file that *exactly* corresponds to R.
@@ -1005,14 +1316,24 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
     // The sugared type is more useful in some cases, and the canonical
     // type in other cases.
     auto Desugared = maybeDesugar(AST, T);
-    std::string TypeName = Desugared.getAsString(TypeHintPolicy);
-    if (T != Desugared && !shouldPrintTypeHint(TypeName)) {
+    std::vector<InlayHintLabelPart> Chunks;
+    TypeInlayHintLabelPartBuilder Builder(
+        Desugared, AST, TypeHintPolicy,
+        /*ShouldAddLinksToTagTypes=*/T != Desugared, Prefix, Chunks);
+    Builder.Visit(Desugared.getTypePtr());
+    if (T != Desugared && !shouldPrintTypeHint(Chunks)) {
       // If the desugared type is too long to display, fallback to the sugared
       // type.
-      TypeName = T.getAsString(TypeHintPolicy);
+      Chunks.clear();
+      TypeInlayHintLabelPartBuilder Builder(T, AST, TypeHintPolicy,
+                                            /*ShouldAddLinksToTagTypes=*/false,
+                                            Prefix, Chunks);
+      Builder.Visit(T.getTypePtr());
     }
-    if (shouldPrintTypeHint(TypeName))
-      addInlayHint(R, HintSide::Right, InlayHintKind::Type, Prefix, TypeName,
+    if (shouldPrintTypeHint(Chunks))
+      addInlayHint(R, HintSide::Right, InlayHintKind::Type,
+                   /*Prefix=*/"", // We have handled prefixes in the builder.
+                   std::move(Chunks),
                    /*Suffix=*/"");
   }
 
@@ -1021,9 +1342,11 @@ class InlayHintVisitor : public RecursiveASTVisitor<InlayHintVisitor> {
                  /*Prefix=*/"", Text, /*Suffix=*/"=");
   }
 
-  bool shouldPrintTypeHint(llvm::StringRef TypeName) const noexcept {
+  bool shouldPrintTypeHint(
+      llvm::ArrayRef<InlayHintLabelPart> TypeLabels) const noexcept {
     return Cfg.InlayHints.TypeNameLimit == 0 ||
-           TypeName.size() < Cfg.InlayHints.TypeNameLimit;
+           lengthOfInlayHintLabelPart(TypeLabels) <
+               Cfg.InlayHints.TypeNameLimit;
   }
 
   void addBlockEndHint(SourceRange BraceRange, StringRef DeclPrefix,
diff --git a/clang-tools-extra/clangd/index/IndexAction.cpp b/clang-tools-extra/clangd/index/IndexAction.cpp
index ed56c2a9d2e811..f3fbb9fc307e2e 100644
--- a/clang-tools-extra/clangd/index/IndexAction.cpp
+++ b/clang-tools-extra/clangd/index/IndexAction.cpp
@@ -32,12 +32,9 @@ namespace clangd {
 namespace {
 
 std::optional<std::string> toURI(OptionalFileEntryRef File) {
-  if (!File)
-    return std::nullopt;
-  auto AbsolutePath = File->getFileEntry().tryGetRealPathName();
-  if (AbsolutePath.empty())
-    return std::nullopt;
-  return URI::create(AbsolutePath).toString();
+  if (auto URI = clang::clangd::toURI(File))
+    return URI->toString();
+  return std::nullopt;
 }
 
 // Collects the nodes and edges of include graph during indexing action.
diff --git a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
index 5b1531eb2fa60b..a69422d825f30b 100644
--- a/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
+++ b/clang-tools-extra/clangd/unittests/InlayHintTests.cpp
@@ -55,6 +55,19 @@ struct ExpectedHint {
   }
 };
 
+struct ExpectedHintLabelPiece {
+  std::string Label;
+  std::optional<std::string> PointsTo = std::nullopt;
+
+  friend llvm::raw_ostream &operator<<(llvm::raw_ostream &Stream,
+                                       const ExpectedHintLabelPiece &Hint) {
+    Stream << Hint.Label;
+    if (Hint.PointsTo)
+      Stream << " that points to $" << Hint.PointsTo;
+    return Stream;
+  }
+};
+
 MATCHER_P2(HintMatcher, Expected, Code, llvm::to_string(Expected)) {
   llvm::StringRef ExpectedView(Expected.Label);
   std::string ResultLabel = arg.joinLabels();
@@ -73,6 +86,34 @@ MATCHER_P2(HintMatcher, Expected, Code, llvm::to_string(Expected)) {
   return true;
 }
 
+MATCHER_P2(HintLabelPieceMatcher, Expected, Code, llvm::to_string(Expected)) {
+  llvm::StringRef ExpectedView(Expected.Label);
+  std::string ResultLabel = arg.value;
+  if (ResultLabel != ExpectedView) {
+    *result_listener << "label is '" << ResultLabel << "'";
+    return false;
+  }
+  if (!Expected.PointsTo && !arg.location)
+    return true;
+  if (Expected.PointsTo && !arg.location) {
+    *result_listener << " range " << *Expected.PointsTo
+                     << " is expected, but we have nothing.";
+    return false;
+  }
+  if (!Expected.PointsTo && arg.location) {
+    *result_listener << " the link points to " << llvm::to_string(arg.location)
+                     << ", but we expect nothing.";
+    return false;
+  }
+  if (arg.location->range != Code.range(*Expected.PointsTo)) {
+    *result_listener << "range is " << llvm::to_string(arg.location->range)
+                     << " but $" << *Expected.PointsTo << " points to "
+                     << llvm::to_string(Code.range(*Expected.PointsTo));
+    return false;
+  }
+  return true;
+}
+
 MATCHER_P(labelIs, Label, "") { return arg.joinLabels() == Label; }
 
 Config noHintsConfig() {
@@ -1637,6 +1678,125 @@ TEST(TypeHints, SubstTemplateParameterAliases) {
                         ExpectedHint{": static_vector<int>", "vector_name"});
 }
 
+template <typename... Labels>
+void assertTypeLinkHints(StringRef Code, StringRef HintRange,
+                         Labels... ExpectedLabels) {
+  Annotations Source(Code);
+  auto HintAt = [&](llvm::ArrayRef<InlayHint> InlayHints,
+                    llvm::StringRef Range) {
+    auto *Hint = llvm::find_if(InlayHints, [&](const InlayHint &InlayHint) {
+      return InlayHint.range == Source.range(Range);
+    });
+    assert(Hint && "No range was found");
+    return llvm::ArrayRef(Hint->label);
+  };
+
+  TestTU TU = TestTU::withCode(Source.code());
+  TU.ExtraArgs.push_back("-std=c++2c");
+  auto AST = TU.build();
+
+  Config C;
+  C.InlayHints.TypeNameLimit = 0;
+  WithContextValue WithCfg(Config::Key, std::move(C));
+
+  auto Hints = hintsOfKind(AST, InlayHintKind::Type);
+  EXPECT_THAT(HintAt(Hints, HintRange),
+              ElementsAre(HintLabelPieceMatcher(ExpectedLabels, Source)...));
+}
+
+TEST(TypeHints, Links) {
+  StringRef Source(R"cpp(
+    $Package[[template <class T, class U>
+    struct Package {]]};
+
+    $SpecializationOfPackage[[template ...
[truncated]

@zyn0217
Copy link
Contributor Author

zyn0217 commented Apr 18, 2024

@HighCommander4 Any chance to have a try on this patch? I'm looking forward to the feedback, thanks!

@HighCommander4
Copy link
Collaborator

Sorry I haven't had a chance to look at this yet. I've been busy with another project, and will continue to be for another week or so. I hope to look at this soon!

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

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

Apologies for the long wait. I'm excited about this enhancement, thanks for working on it!

I've only looked at the tests so far. Will look at the implementation in the coming days but wanted to share my thoughts so far.

@zyn0217
Copy link
Contributor Author

zyn0217 commented May 22, 2024

@HighCommander4 No problem, I'm always delighted to receive responses! However it might take me some time to pick up my thoughts, as the patch has been a while. Anyway, I will try to revive this patch in the next few days and let's see if we can make it into clangd 19.

@zyn0217
Copy link
Contributor Author

zyn0217 commented Jun 16, 2024

@HighCommander4 Sorry for my delay. This is ready for the next round of review, and I'm prepared for a lot of feedback!

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

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

Thanks for the updated patch, this generally looks very nice!

It's a bit unfortunate that we have to reimplement parts of type printing like handling qualifiers and pointers/references. I wouldn't be surprised if we get some edge cases wrong compared to TypePrinter. However, I don't have a better idea currently (other than invasive changes like adding extension points to TypePrinter), and I find two things reassuring:

  1. Our existing TypeHints.* tests exercise the type printing logic of this class, so we know we are not regressing those cases.
  2. If we do regress some edge cases, it's not a big problem, since this feature is just a reading aid and not something critical to correctness.

So, I think it's fine to go with this approach. Thanks for all your efforts putting this together, and sorry it took me this long to get around to revieiwng the implementation!

@zyn0217
Copy link
Contributor Author

zyn0217 commented Jun 19, 2024

Thanks for the prompt and insightful feedback; I really appreciate it! There are a lot of comments, so it takes some time to digest them. I'll make an effort to address them in the coming days.

@zyn0217 zyn0217 requested a review from HighCommander4 July 8, 2024 10:51
@zyn0217
Copy link
Contributor Author

zyn0217 commented Jul 18, 2024

@HighCommander4 Given that this PR has been growing larger, I plan to split it up. Hopefully, this will make the code review easier.

Moreover, I realized it's probably insufficient to build the whole thing on top of a TypeVisitor. Like, a pointer to an array, we have to handle it in two parts so that we are able to put the asterisk in the middle of the array rather than at the end of it. (e.g. int (*)[4] vs int [4] *, where the latter is wrong.) So, we probably have to implement a mechanism similar to TypePrinter.

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

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

(@zyn0217 I'm going to mark this as "Request changes" to reflect my understanding from the previous comment that the next step here is you splitting up the patch. If I've misunderstood and you'd like me to have a look at the current version, let me know!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support go-to-definition on type names in inlay hints
3 participants