Skip to content

Commit a56141b

Browse files
committed
[clangd] Highlight related control flow.
Summary: This means e.g. highlighting "return" will show other returns/throws from the same function, highlighting a case will show all the return/breaks etc. This is a bit of an abuse of textDocument/highlight, but seems useful. Reviewers: adamcz Subscribers: ilya-biryukov, MaskRay, jkorous, mgrang, arphaman, kadircet, usaxena95, cfe-commits Tags: #clang Differential Revision: https://reviews.llvm.org/D78454
1 parent bab5dad commit a56141b

File tree

2 files changed

+426
-17
lines changed

2 files changed

+426
-17
lines changed

clang-tools-extra/clangd/XRefs.cpp

Lines changed: 291 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@
2727
#include "clang/AST/Attrs.inc"
2828
#include "clang/AST/Decl.h"
2929
#include "clang/AST/DeclCXX.h"
30+
#include "clang/AST/DeclObjC.h"
3031
#include "clang/AST/DeclTemplate.h"
3132
#include "clang/AST/ExprCXX.h"
33+
#include "clang/AST/RecursiveASTVisitor.h"
34+
#include "clang/AST/Stmt.h"
35+
#include "clang/AST/StmtCXX.h"
3236
#include "clang/AST/Type.h"
3337
#include "clang/Basic/CharInfo.h"
3438
#include "clang/Basic/LLVM.h"
@@ -45,6 +49,7 @@
4549
#include "llvm/ADT/ArrayRef.h"
4650
#include "llvm/ADT/None.h"
4751
#include "llvm/ADT/STLExtras.h"
52+
#include "llvm/ADT/ScopeExit.h"
4853
#include "llvm/ADT/SmallSet.h"
4954
#include "llvm/ADT/StringExtras.h"
5055
#include "llvm/ADT/StringRef.h"
@@ -712,35 +717,304 @@ findRefs(const std::vector<const NamedDecl *> &Decls, ParsedAST &AST) {
712717
return std::move(RefFinder).take();
713718
}
714719

720+
const Stmt *getFunctionBody(DynTypedNode N) {
721+
if (const auto *FD = N.get<FunctionDecl>())
722+
return FD->getBody();
723+
if (const auto *FD = N.get<BlockDecl>())
724+
return FD->getBody();
725+
if (const auto *FD = N.get<LambdaExpr>())
726+
return FD->getBody();
727+
if (const auto *FD = N.get<ObjCMethodDecl>())
728+
return FD->getBody();
729+
return nullptr;
730+
}
731+
732+
const Stmt *getLoopBody(DynTypedNode N) {
733+
if (const auto *LS = N.get<ForStmt>())
734+
return LS->getBody();
735+
if (const auto *LS = N.get<CXXForRangeStmt>())
736+
return LS->getBody();
737+
if (const auto *LS = N.get<WhileStmt>())
738+
return LS->getBody();
739+
if (const auto *LS = N.get<DoStmt>())
740+
return LS->getBody();
741+
return nullptr;
742+
}
743+
744+
// AST traversal to highlight control flow statements under some root.
745+
// Once we hit further control flow we prune the tree (or at least restrict
746+
// what we highlight) so we capture e.g. breaks from the outer loop only.
747+
class FindControlFlow : public RecursiveASTVisitor<FindControlFlow> {
748+
// Types of control-flow statements we might highlight.
749+
enum Target {
750+
Break = 1,
751+
Continue = 2,
752+
Return = 4,
753+
Case = 8,
754+
Throw = 16,
755+
Goto = 32,
756+
All = Break | Continue | Return | Case | Throw | Goto,
757+
};
758+
int Ignore = 0; // bitmask of Target - what are we *not* highlighting?
759+
SourceRange Bounds; // Half-open, restricts reported targets.
760+
std::vector<SourceLocation> &Result;
761+
const SourceManager &SM;
762+
763+
// Masks out targets for a traversal into D.
764+
// Traverses the subtree using Delegate() if any targets remain.
765+
template <typename Func>
766+
bool filterAndTraverse(DynTypedNode D, const Func &Delegate) {
767+
auto RestoreIgnore = llvm::make_scope_exit(
768+
[OldIgnore(Ignore), this] { Ignore = OldIgnore; });
769+
if (getFunctionBody(D))
770+
Ignore = All;
771+
else if (getLoopBody(D))
772+
Ignore |= Continue | Break;
773+
else if (D.get<SwitchStmt>())
774+
Ignore |= Break | Case;
775+
// Prune tree if we're not looking for anything.
776+
return (Ignore == All) ? true : Delegate();
777+
}
778+
779+
void found(Target T, SourceLocation Loc) {
780+
if (T & Ignore)
781+
return;
782+
if (SM.isBeforeInTranslationUnit(Loc, Bounds.getBegin()) ||
783+
SM.isBeforeInTranslationUnit(Bounds.getEnd(), Loc))
784+
return;
785+
Result.push_back(Loc);
786+
}
787+
788+
public:
789+
FindControlFlow(SourceRange Bounds, std::vector<SourceLocation> &Result,
790+
const SourceManager &SM)
791+
: Bounds(Bounds), Result(Result), SM(SM) {}
792+
793+
// When traversing function or loops, limit targets to those that still
794+
// refer to the original root.
795+
bool TraverseDecl(Decl *D) {
796+
return !D || filterAndTraverse(DynTypedNode::create(*D), [&] {
797+
return RecursiveASTVisitor::TraverseDecl(D);
798+
});
799+
}
800+
bool TraverseStmt(Stmt *S) {
801+
return !S || filterAndTraverse(DynTypedNode::create(*S), [&] {
802+
return RecursiveASTVisitor::TraverseStmt(S);
803+
});
804+
}
805+
806+
// Add leaves that we found and want.
807+
bool VisitReturnStmt(ReturnStmt *R) {
808+
found(Return, R->getReturnLoc());
809+
return true;
810+
}
811+
bool VisitBreakStmt(BreakStmt *B) {
812+
found(Break, B->getBreakLoc());
813+
return true;
814+
}
815+
bool VisitContinueStmt(ContinueStmt *C) {
816+
found(Continue, C->getContinueLoc());
817+
return true;
818+
}
819+
bool VisitSwitchCase(SwitchCase *C) {
820+
found(Case, C->getKeywordLoc());
821+
return true;
822+
}
823+
bool VisitCXXThrowExpr(CXXThrowExpr *T) {
824+
found(Throw, T->getThrowLoc());
825+
return true;
826+
}
827+
bool VisitGotoStmt(GotoStmt *G) {
828+
// Goto is interesting if its target is outside the root.
829+
if (const auto *LD = G->getLabel()) {
830+
if (SM.isBeforeInTranslationUnit(LD->getLocation(), Bounds.getBegin()) ||
831+
SM.isBeforeInTranslationUnit(Bounds.getEnd(), LD->getLocation()))
832+
found(Goto, G->getGotoLoc());
833+
}
834+
return true;
835+
}
836+
};
837+
838+
// Given a location within a switch statement, return the half-open range that
839+
// covers the case it's contained in.
840+
// We treat `case X: case Y: ...` as one case, and assume no other fallthrough.
841+
SourceRange findCaseBounds(const SwitchStmt &Switch, SourceLocation Loc,
842+
const SourceManager &SM) {
843+
// Cases are not stored in order, sort them first.
844+
// (In fact they seem to be stored in reverse order, don't rely on this)
845+
std::vector<const SwitchCase *> Cases;
846+
for (const SwitchCase *Case = Switch.getSwitchCaseList(); Case;
847+
Case = Case->getNextSwitchCase())
848+
Cases.push_back(Case);
849+
llvm::sort(Cases, [&](const SwitchCase *L, const SwitchCase *R) {
850+
return SM.isBeforeInTranslationUnit(L->getKeywordLoc(), R->getKeywordLoc());
851+
});
852+
853+
// Find the first case after the target location, the end of our range.
854+
auto CaseAfter = llvm::partition_point(Cases, [&](const SwitchCase *C) {
855+
return !SM.isBeforeInTranslationUnit(Loc, C->getKeywordLoc());
856+
});
857+
SourceLocation End = CaseAfter == Cases.end() ? Switch.getEndLoc()
858+
: (*CaseAfter)->getKeywordLoc();
859+
860+
// Our target can be before the first case - cases are optional!
861+
if (CaseAfter == Cases.begin())
862+
return SourceRange(Switch.getBeginLoc(), End);
863+
// The start of our range is usually the previous case, but...
864+
auto CaseBefore = std::prev(CaseAfter);
865+
// ... rewind CaseBefore to the first in a `case A: case B: ...` sequence.
866+
while (CaseBefore != Cases.begin() &&
867+
(*std::prev(CaseBefore))->getSubStmt() == *CaseBefore)
868+
--CaseBefore;
869+
return SourceRange((*CaseBefore)->getKeywordLoc(), End);
870+
}
871+
872+
// Returns the locations of control flow statements related to N. e.g.:
873+
// for => branches: break/continue/return/throw
874+
// break => controlling loop (forwhile/do), and its related control flow
875+
// return => all returns/throws from the same function
876+
// When an inner block is selected, we include branches bound to outer blocks
877+
// as these are exits from the inner block. e.g. return in a for loop.
878+
// FIXME: We don't analyze catch blocks, throw is treated the same as return.
879+
std::vector<SourceLocation> relatedControlFlow(const SelectionTree::Node &N) {
880+
const SourceManager &SM =
881+
N.getDeclContext().getParentASTContext().getSourceManager();
882+
std::vector<SourceLocation> Result;
883+
884+
// First, check if we're at a node that can resolve to a root.
885+
enum class Cur { None, Break, Continue, Return, Case, Throw } Cursor;
886+
if (N.ASTNode.get<BreakStmt>()) {
887+
Cursor = Cur::Break;
888+
} else if (N.ASTNode.get<ContinueStmt>()) {
889+
Cursor = Cur::Continue;
890+
} else if (N.ASTNode.get<ReturnStmt>()) {
891+
Cursor = Cur::Return;
892+
} else if (N.ASTNode.get<CXXThrowExpr>()) {
893+
Cursor = Cur::Throw;
894+
} else if (N.ASTNode.get<SwitchCase>()) {
895+
Cursor = Cur::Case;
896+
} else if (const GotoStmt *GS = N.ASTNode.get<GotoStmt>()) {
897+
// We don't know what root to associate with, but highlight the goto/label.
898+
Result.push_back(GS->getGotoLoc());
899+
if (const auto *LD = GS->getLabel())
900+
Result.push_back(LD->getLocation());
901+
Cursor = Cur::None;
902+
} else {
903+
Cursor = Cur::None;
904+
}
905+
906+
const Stmt *Root = nullptr; // Loop or function body to traverse.
907+
SourceRange Bounds;
908+
// Look up the tree for a root (or just at this node if we didn't find a leaf)
909+
for (const auto *P = &N; P; P = P->Parent) {
910+
// return associates with enclosing function
911+
if (const Stmt *FunctionBody = getFunctionBody(P->ASTNode)) {
912+
if (Cursor == Cur::Return || Cursor == Cur::Throw) {
913+
Root = FunctionBody;
914+
}
915+
break; // other leaves don't cross functions.
916+
}
917+
// break/continue associate with enclosing loop.
918+
if (const Stmt *LoopBody = getLoopBody(P->ASTNode)) {
919+
if (Cursor == Cur::None || Cursor == Cur::Break ||
920+
Cursor == Cur::Continue) {
921+
Root = LoopBody;
922+
// Highlight the loop keyword itself.
923+
// FIXME: for do-while, this only covers the `do`..
924+
Result.push_back(P->ASTNode.getSourceRange().getBegin());
925+
break;
926+
}
927+
}
928+
// For switches, users think of case statements as control flow blocks.
929+
// We highlight only occurrences surrounded by the same case.
930+
// We don't detect fallthrough (other than 'case X, case Y').
931+
if (const auto *SS = P->ASTNode.get<SwitchStmt>()) {
932+
if (Cursor == Cur::Break || Cursor == Cur::Case) {
933+
Result.push_back(SS->getSwitchLoc()); // Highlight the switch.
934+
Root = SS->getBody();
935+
// Limit to enclosing case, if there is one.
936+
Bounds = findCaseBounds(*SS, N.ASTNode.getSourceRange().getBegin(), SM);
937+
break;
938+
}
939+
}
940+
// If we didn't start at some interesting node, we're done.
941+
if (Cursor == Cur::None)
942+
break;
943+
}
944+
if (Root) {
945+
if (!Bounds.isValid())
946+
Bounds = Root->getSourceRange();
947+
FindControlFlow(Bounds, Result, SM).TraverseStmt(const_cast<Stmt *>(Root));
948+
}
949+
return Result;
950+
}
951+
952+
DocumentHighlight toHighlight(const ReferenceFinder::Reference &Ref,
953+
const SourceManager &SM) {
954+
DocumentHighlight DH;
955+
DH.range = Ref.range(SM);
956+
if (Ref.Role & index::SymbolRoleSet(index::SymbolRole::Write))
957+
DH.kind = DocumentHighlightKind::Write;
958+
else if (Ref.Role & index::SymbolRoleSet(index::SymbolRole::Read))
959+
DH.kind = DocumentHighlightKind::Read;
960+
else
961+
DH.kind = DocumentHighlightKind::Text;
962+
return DH;
963+
}
964+
965+
llvm::Optional<DocumentHighlight> toHighlight(SourceLocation Loc,
966+
const syntax::TokenBuffer &TB) {
967+
Loc = TB.sourceManager().getFileLoc(Loc);
968+
if (const auto *Tok = TB.spelledTokenAt(Loc)) {
969+
DocumentHighlight Result;
970+
Result.range = halfOpenToRange(
971+
TB.sourceManager(),
972+
CharSourceRange::getCharRange(Tok->location(), Tok->endLocation()));
973+
return Result;
974+
}
975+
return llvm::None;
976+
}
977+
715978
} // namespace
716979

717980
std::vector<DocumentHighlight> findDocumentHighlights(ParsedAST &AST,
718981
Position Pos) {
719982
const SourceManager &SM = AST.getSourceManager();
720983
// FIXME: show references to macro within file?
721-
DeclRelationSet Relations =
722-
DeclRelation::TemplatePattern | DeclRelation::Alias;
723984
auto CurLoc = sourceLocationInMainFile(SM, Pos);
724985
if (!CurLoc) {
725986
llvm::consumeError(CurLoc.takeError());
726987
return {};
727988
}
728-
auto References = findRefs(getDeclAtPosition(AST, *CurLoc, Relations), AST);
729-
730-
// FIXME: we may get multiple DocumentHighlights with the same location and
731-
// different kinds, deduplicate them.
732989
std::vector<DocumentHighlight> Result;
733-
for (const auto &Ref : References) {
734-
DocumentHighlight DH;
735-
DH.range = Ref.range(SM);
736-
if (Ref.Role & index::SymbolRoleSet(index::SymbolRole::Write))
737-
DH.kind = DocumentHighlightKind::Write;
738-
else if (Ref.Role & index::SymbolRoleSet(index::SymbolRole::Read))
739-
DH.kind = DocumentHighlightKind::Read;
740-
else
741-
DH.kind = DocumentHighlightKind::Text;
742-
Result.push_back(std::move(DH));
743-
}
990+
auto TryTree = [&](SelectionTree ST) {
991+
if (const SelectionTree::Node *N = ST.commonAncestor()) {
992+
DeclRelationSet Relations =
993+
DeclRelation::TemplatePattern | DeclRelation::Alias;
994+
auto Decls = targetDecl(N->ASTNode, Relations);
995+
if (!Decls.empty()) {
996+
auto Refs = findRefs({Decls.begin(), Decls.end()}, AST);
997+
// FIXME: we may get multiple DocumentHighlights with the same location
998+
// and different kinds, deduplicate them.
999+
for (const auto &Ref : findRefs({Decls.begin(), Decls.end()}, AST))
1000+
Result.push_back(toHighlight(Ref, SM));
1001+
return true;
1002+
}
1003+
auto ControlFlow = relatedControlFlow(*N);
1004+
if (!ControlFlow.empty()) {
1005+
for (SourceLocation Loc : ControlFlow)
1006+
if (auto Highlight = toHighlight(Loc, AST.getTokens()))
1007+
Result.push_back(std::move(*Highlight));
1008+
return true;
1009+
}
1010+
}
1011+
return false;
1012+
};
1013+
1014+
unsigned Offset =
1015+
AST.getSourceManager().getDecomposedSpellingLoc(*CurLoc).second;
1016+
SelectionTree::createEach(AST.getASTContext(), AST.getTokens(), Offset,
1017+
Offset, TryTree);
7441018
return Result;
7451019
}
7461020

0 commit comments

Comments
 (0)