[clangd] Show comment of field on hover over accessor (#182738)
When synthesizing the documentation of a trivial getter / setter also include the documentation of that field in the hover info.
This commit is contained in:
parent
ce9d033c51
commit
3a526fba72
2 changed files with 119 additions and 37 deletions
|
|
@ -511,108 +511,147 @@ PrintExprResult printExprValue(const SelectionTree::Node *N,
|
|||
/*Node=*/N};
|
||||
}
|
||||
|
||||
std::optional<StringRef> fieldName(const Expr *E) {
|
||||
// Returns the FieldDecl if E is of the form `this->field`, otherwise nullptr.
|
||||
const FieldDecl *fieldDecl(const Expr *E) {
|
||||
const auto *ME = llvm::dyn_cast<MemberExpr>(E->IgnoreCasts());
|
||||
if (!ME || !llvm::isa<CXXThisExpr>(ME->getBase()->IgnoreCasts()))
|
||||
return std::nullopt;
|
||||
const auto *Field = llvm::dyn_cast<FieldDecl>(ME->getMemberDecl());
|
||||
return nullptr;
|
||||
return llvm::dyn_cast<FieldDecl>(ME->getMemberDecl());
|
||||
}
|
||||
|
||||
std::optional<StringRef> fieldName(const Expr *E) {
|
||||
const auto *Field = fieldDecl(E);
|
||||
if (!Field || !Field->getDeclName().isIdentifier())
|
||||
return std::nullopt;
|
||||
return Field->getDeclName().getAsIdentifierInfo()->getName();
|
||||
}
|
||||
|
||||
// If CMD is of the form T foo() { return FieldName; } then returns "FieldName".
|
||||
std::optional<StringRef> getterVariableName(const CXXMethodDecl *CMD) {
|
||||
std::optional<std::string> fieldComment(const ASTContext &Ctx, const Expr *E) {
|
||||
const auto *Field = fieldDecl(E);
|
||||
if (!Field)
|
||||
return std::nullopt;
|
||||
const auto Comment = getDeclComment(Ctx, *Field);
|
||||
if (Comment.empty())
|
||||
return std::nullopt;
|
||||
return Comment;
|
||||
}
|
||||
|
||||
// Returns the returned expression of a trivial getter body, or nullptr if the
|
||||
// method does not match the pattern T foo() { return FieldName; }.
|
||||
const Expr *getterReturnExpr(const CXXMethodDecl *CMD) {
|
||||
assert(CMD->hasBody());
|
||||
if (CMD->getNumParams() != 0 || CMD->isVariadic())
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
const auto *Body = llvm::dyn_cast<CompoundStmt>(CMD->getBody());
|
||||
const auto *OnlyReturn = (Body && Body->size() == 1)
|
||||
? llvm::dyn_cast<ReturnStmt>(Body->body_front())
|
||||
: nullptr;
|
||||
if (!OnlyReturn || !OnlyReturn->getRetValue())
|
||||
return std::nullopt;
|
||||
return fieldName(OnlyReturn->getRetValue());
|
||||
return nullptr;
|
||||
return OnlyReturn->getRetValue();
|
||||
}
|
||||
|
||||
// If CMD is one of the forms:
|
||||
// void foo(T arg) { FieldName = arg; }
|
||||
// R foo(T arg) { FieldName = arg; return *this; }
|
||||
// R* foo(T arg) { FieldName = arg; return this; }
|
||||
// R& foo(T arg) { FieldName = arg; return *this; }
|
||||
// void foo(T arg) { FieldName = std::move(arg); }
|
||||
// R foo(T arg) { FieldName = std::move(arg); return *this; }
|
||||
// then returns "FieldName"
|
||||
std::optional<StringRef> setterVariableName(const CXXMethodDecl *CMD) {
|
||||
// R* foo(T arg) { FieldName = std::move(arg); return this; }
|
||||
// R& foo(T arg) { FieldName = std::move(arg); return *this; }
|
||||
// returns the LHS expression (FieldName) of the assignment in a trivial setter
|
||||
// body, or nullptr if the method does not match the pattern of a trivial
|
||||
// setter.
|
||||
const Expr *setterLHS(const CXXMethodDecl *CMD) {
|
||||
assert(CMD->hasBody());
|
||||
if (CMD->isConst() || CMD->getNumParams() != 1 || CMD->isVariadic())
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
const ParmVarDecl *Arg = CMD->getParamDecl(0);
|
||||
if (Arg->isParameterPack())
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
|
||||
const auto *Body = llvm::dyn_cast<CompoundStmt>(CMD->getBody());
|
||||
if (!Body || Body->size() == 0 || Body->size() > 2)
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
// If the second statement exists, it must be `return this` or `return *this`.
|
||||
if (Body->size() == 2) {
|
||||
auto *Ret = llvm::dyn_cast<ReturnStmt>(Body->body_back());
|
||||
if (!Ret || !Ret->getRetValue())
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
const Expr *RetVal = Ret->getRetValue()->IgnoreCasts();
|
||||
if (const auto *UO = llvm::dyn_cast<UnaryOperator>(RetVal)) {
|
||||
if (UO->getOpcode() != UO_Deref)
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
RetVal = UO->getSubExpr()->IgnoreCasts();
|
||||
}
|
||||
if (!llvm::isa<CXXThisExpr>(RetVal))
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
// The first statement must be an assignment of the arg to a field.
|
||||
const Expr *LHS, *RHS;
|
||||
if (const auto *BO = llvm::dyn_cast<BinaryOperator>(Body->body_front())) {
|
||||
if (BO->getOpcode() != BO_Assign)
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
LHS = BO->getLHS();
|
||||
RHS = BO->getRHS();
|
||||
} else if (const auto *COCE =
|
||||
llvm::dyn_cast<CXXOperatorCallExpr>(Body->body_front())) {
|
||||
if (COCE->getOperator() != OO_Equal || COCE->getNumArgs() != 2)
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
LHS = COCE->getArg(0);
|
||||
RHS = COCE->getArg(1);
|
||||
} else {
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Detect the case when the item is moved into the field.
|
||||
if (auto *CE = llvm::dyn_cast<CallExpr>(RHS->IgnoreCasts())) {
|
||||
if (CE->getNumArgs() != 1)
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
auto *ND = llvm::dyn_cast_or_null<NamedDecl>(CE->getCalleeDecl());
|
||||
if (!ND || !ND->getIdentifier() || ND->getName() != "move" ||
|
||||
!ND->isInStdNamespace())
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
RHS = CE->getArg(0);
|
||||
}
|
||||
|
||||
auto *DRE = llvm::dyn_cast<DeclRefExpr>(RHS->IgnoreCasts());
|
||||
if (!DRE || DRE->getDecl() != Arg)
|
||||
return std::nullopt;
|
||||
return fieldName(LHS);
|
||||
return nullptr;
|
||||
return LHS;
|
||||
}
|
||||
|
||||
std::string synthesizeDocumentation(const NamedDecl *ND) {
|
||||
if (const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(ND)) {
|
||||
// Is this an ordinary, non-static method whose definition is visible?
|
||||
if (CMD->getDeclName().isIdentifier() && !CMD->isStatic() &&
|
||||
(CMD = llvm::dyn_cast_or_null<CXXMethodDecl>(CMD->getDefinition())) &&
|
||||
CMD->hasBody()) {
|
||||
if (const auto GetterField = getterVariableName(CMD))
|
||||
return llvm::formatv("Trivial accessor for `{0}`.", *GetterField);
|
||||
if (const auto SetterField = setterVariableName(CMD))
|
||||
return llvm::formatv("Trivial setter for `{0}`.", *SetterField);
|
||||
std::string synthesizeDocumentation(const ASTContext &Ctx,
|
||||
const NamedDecl *ND) {
|
||||
const auto *CMD = llvm::dyn_cast<CXXMethodDecl>(ND);
|
||||
if (!CMD)
|
||||
return {};
|
||||
|
||||
// Is this an ordinary, non-static method whose definition is visible?
|
||||
if (!CMD->getDeclName().isIdentifier() || CMD->isStatic())
|
||||
return {};
|
||||
|
||||
CMD = llvm::dyn_cast_or_null<CXXMethodDecl>(CMD->getDefinition());
|
||||
if (!CMD || !CMD->hasBody())
|
||||
return {};
|
||||
|
||||
if (const Expr *RetVal = getterReturnExpr(CMD)) {
|
||||
if (const auto GetterField = fieldName(RetVal)) {
|
||||
if (const auto Comment = fieldComment(Ctx, RetVal))
|
||||
return llvm::formatv("Trivial accessor for `{0}`.\n\n{1}", *GetterField,
|
||||
*Comment);
|
||||
return llvm::formatv("Trivial accessor for `{0}`.", *GetterField);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
if (const auto *const SetterLHS = setterLHS(CMD)) {
|
||||
if (const auto FieldName = fieldName(SetterLHS)) {
|
||||
if (const auto Comment = fieldComment(Ctx, SetterLHS))
|
||||
return llvm::formatv("Trivial setter for `{0}`.\n\n{1}", *FieldName,
|
||||
*Comment);
|
||||
return llvm::formatv("Trivial setter for `{0}`.", *FieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Generate a \p Hover object given the declaration \p D.
|
||||
|
|
@ -638,7 +677,7 @@ HoverInfo getHoverContents(const NamedDecl *D, const PrintingPolicy &PP,
|
|||
HI.CommentOpts = D->getASTContext().getLangOpts().CommentOpts;
|
||||
enhanceFromIndex(HI, *CommentD, Index);
|
||||
if (HI.Documentation.empty())
|
||||
HI.Documentation = synthesizeDocumentation(D);
|
||||
HI.Documentation = synthesizeDocumentation(Ctx, D);
|
||||
|
||||
HI.Kind = index::getSymbolInfo(D).Kind;
|
||||
|
||||
|
|
|
|||
|
|
@ -1016,6 +1016,26 @@ class Foo final {})cpp";
|
|||
HI.Parameters.emplace();
|
||||
HI.AccessSpecifier = "public";
|
||||
}},
|
||||
{// Getter with comment
|
||||
R"cpp(
|
||||
struct X {
|
||||
// An int named Y
|
||||
int Y;
|
||||
float [[^y]]() { return Y; }
|
||||
};
|
||||
)cpp",
|
||||
[](HoverInfo &HI) {
|
||||
HI.Name = "y";
|
||||
HI.Kind = index::SymbolKind::InstanceMethod;
|
||||
HI.NamespaceScope = "";
|
||||
HI.Definition = "float y()";
|
||||
HI.LocalScope = "X::";
|
||||
HI.Documentation = "Trivial accessor for `Y`.\n\nAn int named Y";
|
||||
HI.Type = "float ()";
|
||||
HI.ReturnType = "float";
|
||||
HI.Parameters.emplace();
|
||||
HI.AccessSpecifier = "public";
|
||||
}},
|
||||
{// Setter
|
||||
R"cpp(
|
||||
struct X { int Y; void [[^setY]](float v) { Y = v; } };
|
||||
|
|
@ -1074,6 +1094,29 @@ class Foo final {})cpp";
|
|||
HI.Parameters->back().Name = "v";
|
||||
HI.AccessSpecifier = "public";
|
||||
}},
|
||||
{// Setter with comment
|
||||
R"cpp(
|
||||
struct X {
|
||||
// An int named Y
|
||||
int Y;
|
||||
void [[^setY]](float v) { Y = v; }
|
||||
};
|
||||
)cpp",
|
||||
[](HoverInfo &HI) {
|
||||
HI.Name = "setY";
|
||||
HI.Kind = index::SymbolKind::InstanceMethod;
|
||||
HI.NamespaceScope = "";
|
||||
HI.Definition = "void setY(float v)";
|
||||
HI.LocalScope = "X::";
|
||||
HI.Documentation = "Trivial setter for `Y`.\n\nAn int named Y";
|
||||
HI.Type = "void (float)";
|
||||
HI.ReturnType = "void";
|
||||
HI.Parameters.emplace();
|
||||
HI.Parameters->emplace_back();
|
||||
HI.Parameters->back().Type = "float";
|
||||
HI.Parameters->back().Name = "v";
|
||||
HI.AccessSpecifier = "public";
|
||||
}},
|
||||
{// Field type initializer.
|
||||
R"cpp(
|
||||
struct X { int x = 2; };
|
||||
|
|
|
|||
Loading…
Reference in a new issue