Skip to content

Commit 4399878

Browse files
committed
[clangd] A code action to swap branches of an if statement
Reviewers: sammccall Reviewed By: sammccall Subscribers: llvm-commits, mgorny, ioeric, MaskRay, jkorous, arphaman, kadircet, cfe-commits Tags: #clang Differential Revision: https://reviews.llvm.org/D56611 llvm-svn: 352796
1 parent 0bd6b91 commit 4399878

File tree

7 files changed

+401
-13
lines changed

7 files changed

+401
-13
lines changed

clang-tools-extra/clangd/SourceCode.cpp

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include "clang/AST/ASTContext.h"
1212
#include "clang/Basic/SourceManager.h"
1313
#include "clang/Lex/Lexer.h"
14+
#include "llvm/ADT/None.h"
15+
#include "llvm/ADT/StringRef.h"
1416
#include "llvm/Support/Errc.h"
1517
#include "llvm/Support/Error.h"
1618
#include "llvm/Support/Path.h"
@@ -141,6 +143,69 @@ Position sourceLocToPosition(const SourceManager &SM, SourceLocation Loc) {
141143
return P;
142144
}
143145

146+
bool isValidFileRange(const SourceManager &Mgr, SourceRange R) {
147+
if (!R.getBegin().isValid() || !R.getEnd().isValid())
148+
return false;
149+
150+
FileID BeginFID;
151+
size_t BeginOffset = 0;
152+
std::tie(BeginFID, BeginOffset) = Mgr.getDecomposedLoc(R.getBegin());
153+
154+
FileID EndFID;
155+
size_t EndOffset = 0;
156+
std::tie(EndFID, EndOffset) = Mgr.getDecomposedLoc(R.getEnd());
157+
158+
return BeginFID.isValid() && BeginFID == EndFID && BeginOffset <= EndOffset;
159+
}
160+
161+
bool halfOpenRangeContains(const SourceManager &Mgr, SourceRange R,
162+
SourceLocation L) {
163+
assert(isValidFileRange(Mgr, R));
164+
165+
FileID BeginFID;
166+
size_t BeginOffset = 0;
167+
std::tie(BeginFID, BeginOffset) = Mgr.getDecomposedLoc(R.getBegin());
168+
size_t EndOffset = Mgr.getFileOffset(R.getEnd());
169+
170+
FileID LFid;
171+
size_t LOffset;
172+
std::tie(LFid, LOffset) = Mgr.getDecomposedLoc(L);
173+
return BeginFID == LFid && BeginOffset <= LOffset && LOffset < EndOffset;
174+
}
175+
176+
bool halfOpenRangeTouches(const SourceManager &Mgr, SourceRange R,
177+
SourceLocation L) {
178+
return L == R.getEnd() || halfOpenRangeContains(Mgr, R, L);
179+
}
180+
181+
llvm::Optional<SourceRange> toHalfOpenFileRange(const SourceManager &Mgr,
182+
const LangOptions &LangOpts,
183+
SourceRange R) {
184+
auto Begin = Mgr.getFileLoc(R.getBegin());
185+
if (Begin.isInvalid())
186+
return llvm::None;
187+
auto End = Mgr.getFileLoc(R.getEnd());
188+
if (End.isInvalid())
189+
return llvm::None;
190+
End = Lexer::getLocForEndOfToken(End, 0, Mgr, LangOpts);
191+
192+
SourceRange Result(Begin, End);
193+
if (!isValidFileRange(Mgr, Result))
194+
return llvm::None;
195+
return Result;
196+
}
197+
198+
llvm::StringRef toSourceCode(const SourceManager &SM, SourceRange R) {
199+
assert(isValidFileRange(SM, R));
200+
bool Invalid = false;
201+
auto *Buf = SM.getBuffer(SM.getFileID(R.getBegin()), &Invalid);
202+
assert(!Invalid);
203+
204+
size_t BeginOffset = SM.getFileOffset(R.getBegin());
205+
size_t EndOffset = SM.getFileOffset(R.getEnd());
206+
return Buf->getBuffer().substr(BeginOffset, EndOffset - BeginOffset);
207+
}
208+
144209
llvm::Expected<SourceLocation> sourceLocationInMainFile(const SourceManager &SM,
145210
Position P) {
146211
llvm::StringRef Code = SM.getBuffer(SM.getMainFileID())->getBuffer();
@@ -169,8 +234,7 @@ std::pair<size_t, size_t> offsetToClangLineColumn(llvm::StringRef Code,
169234
return {Lines + 1, Offset - StartOfLine + 1};
170235
}
171236

172-
std::pair<llvm::StringRef, llvm::StringRef>
173-
splitQualifiedName(llvm::StringRef QName) {
237+
std::pair<StringRef, StringRef> splitQualifiedName(StringRef QName) {
174238
size_t Pos = QName.rfind("::");
175239
if (Pos == llvm::StringRef::npos)
176240
return {llvm::StringRef(), QName};

clang-tools-extra/clangd/SourceCode.h

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_SOURCECODE_H
1515
#include "Protocol.h"
1616
#include "clang/Basic/Diagnostic.h"
17+
#include "clang/Basic/LangOptions.h"
1718
#include "clang/Basic/SourceLocation.h"
1819
#include "clang/Basic/SourceManager.h"
1920
#include "clang/Format/Format.h"
@@ -61,6 +62,46 @@ Position sourceLocToPosition(const SourceManager &SM, SourceLocation Loc);
6162
llvm::Expected<SourceLocation> sourceLocationInMainFile(const SourceManager &SM,
6263
Position P);
6364

65+
/// Turns a token range into a half-open range and checks its correctness.
66+
/// The resulting range will have only valid source location on both sides, both
67+
/// of which are file locations.
68+
///
69+
/// File locations always point to a particular offset in a file, i.e. they
70+
/// never refer to a location inside a macro expansion. Turning locations from
71+
/// macro expansions into file locations is ambiguous - one can use
72+
/// SourceManager::{getExpansion|getFile|getSpelling}Loc. This function
73+
/// calls SourceManager::getFileLoc on both ends of \p R to do the conversion.
74+
///
75+
/// User input (e.g. cursor position) is expressed as a file location, so this
76+
/// function can be viewed as a way to normalize the ranges used in the clang
77+
/// AST so that they are comparable with ranges coming from the user input.
78+
llvm::Optional<SourceRange> toHalfOpenFileRange(const SourceManager &Mgr,
79+
const LangOptions &LangOpts,
80+
SourceRange R);
81+
82+
/// Returns true iff all of the following conditions hold:
83+
/// - start and end locations are valid,
84+
/// - start and end locations are file locations from the same file
85+
/// (i.e. expansion locations are not taken into account).
86+
/// - start offset <= end offset.
87+
/// FIXME: introduce a type for source range with this invariant.
88+
bool isValidFileRange(const SourceManager &Mgr, SourceRange R);
89+
90+
/// Returns true iff \p L is contained in \p R.
91+
/// EXPECTS: isValidFileRange(R) == true, L is a file location.
92+
bool halfOpenRangeContains(const SourceManager &Mgr, SourceRange R,
93+
SourceLocation L);
94+
95+
/// Returns true iff \p L is contained in \p R or \p L is equal to the end point
96+
/// of \p R.
97+
/// EXPECTS: isValidFileRange(R) == true, L is a file location.
98+
bool halfOpenRangeTouches(const SourceManager &Mgr, SourceRange R,
99+
SourceLocation L);
100+
101+
/// Returns the source code covered by the source range.
102+
/// EXPECTS: isValidFileRange(R) == true.
103+
llvm::StringRef toSourceCode(const SourceManager &SM, SourceRange R);
104+
64105
// Converts a half-open clang source range to an LSP range.
65106
// Note that clang also uses closed source ranges, which this can't handle!
66107
Range halfOpenToRange(const SourceManager &SM, CharSourceRange R);

clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../..)
88
# $<TARGET_OBJECTS:obj.clangDaemonTweaks> to a list of sources, see
99
# clangd/tool/CMakeLists.txt for an example.
1010
add_clang_library(clangDaemonTweaks OBJECT
11-
Dummy.cpp # FIXME: to avoid CMake errors due to empty inputs, remove when a
12-
# first tweak lands.
11+
SwapIfBranches.cpp
1312
)

clang-tools-extra/clangd/refactor/tweaks/Dummy.cpp

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===--- SwapIfBranches.cpp --------------------------------------*- C++-*-===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
#include "ClangdUnit.h"
9+
#include "Logger.h"
10+
#include "SourceCode.h"
11+
#include "refactor/Tweak.h"
12+
#include "clang/AST/ASTContext.h"
13+
#include "clang/AST/RecursiveASTVisitor.h"
14+
#include "clang/AST/Stmt.h"
15+
#include "clang/Basic/LangOptions.h"
16+
#include "clang/Basic/SourceLocation.h"
17+
#include "clang/Basic/SourceManager.h"
18+
#include "clang/Lex/Lexer.h"
19+
#include "clang/Tooling/Core/Replacement.h"
20+
#include "llvm/ADT/None.h"
21+
#include "llvm/ADT/Optional.h"
22+
#include "llvm/ADT/StringRef.h"
23+
#include "llvm/Support/Casting.h"
24+
#include "llvm/Support/Error.h"
25+
26+
namespace clang {
27+
namespace clangd {
28+
namespace {
29+
/// Swaps the 'then' and 'else' branch of the if statement.
30+
/// Before:
31+
/// if (foo) { return 10; } else { continue; }
32+
/// ^^^^^^^ ^^^^
33+
/// After:
34+
/// if (foo) { continue; } else { return 10; }
35+
class SwapIfBranches : public Tweak {
36+
public:
37+
TweakID id() const override final;
38+
39+
bool prepare(const Selection &Inputs) override;
40+
Expected<tooling::Replacements> apply(const Selection &Inputs) override;
41+
std::string title() const override;
42+
43+
private:
44+
IfStmt *If = nullptr;
45+
};
46+
47+
REGISTER_TWEAK(SwapIfBranches);
48+
49+
class FindIfUnderCursor : public RecursiveASTVisitor<FindIfUnderCursor> {
50+
public:
51+
FindIfUnderCursor(ASTContext &Ctx, SourceLocation CursorLoc, IfStmt *&Result)
52+
: Ctx(Ctx), CursorLoc(CursorLoc), Result(Result) {}
53+
54+
bool VisitIfStmt(IfStmt *If) {
55+
// Check if the cursor is in the range of 'if (cond)'.
56+
// FIXME: this does not contain the closing paren, add it too.
57+
auto R = toHalfOpenFileRange(
58+
Ctx.getSourceManager(), Ctx.getLangOpts(),
59+
SourceRange(If->getIfLoc(), If->getCond()->getEndLoc().isValid()
60+
? If->getCond()->getEndLoc()
61+
: If->getIfLoc()));
62+
if (R && halfOpenRangeTouches(Ctx.getSourceManager(), *R, CursorLoc)) {
63+
Result = If;
64+
return false;
65+
}
66+
// Check the range of 'else'.
67+
R = toHalfOpenFileRange(Ctx.getSourceManager(), Ctx.getLangOpts(),
68+
SourceRange(If->getElseLoc()));
69+
if (R && halfOpenRangeTouches(Ctx.getSourceManager(), *R, CursorLoc)) {
70+
Result = If;
71+
return false;
72+
}
73+
74+
return true;
75+
}
76+
77+
private:
78+
ASTContext &Ctx;
79+
SourceLocation CursorLoc;
80+
IfStmt *&Result;
81+
};
82+
} // namespace
83+
84+
bool SwapIfBranches::prepare(const Selection &Inputs) {
85+
auto &Ctx = Inputs.AST.getASTContext();
86+
FindIfUnderCursor(Ctx, Inputs.Cursor, If).TraverseAST(Ctx);
87+
if (!If)
88+
return false;
89+
90+
// avoid dealing with single-statement brances, they require careful handling
91+
// to avoid changing semantics of the code (i.e. dangling else).
92+
if (!If->getThen() || !llvm::isa<CompoundStmt>(If->getThen()) ||
93+
!If->getElse() || !llvm::isa<CompoundStmt>(If->getElse()))
94+
return false;
95+
return true;
96+
}
97+
98+
Expected<tooling::Replacements> SwapIfBranches::apply(const Selection &Inputs) {
99+
auto &Ctx = Inputs.AST.getASTContext();
100+
auto &SrcMgr = Ctx.getSourceManager();
101+
102+
auto ThenRng = toHalfOpenFileRange(SrcMgr, Ctx.getLangOpts(),
103+
If->getThen()->getSourceRange());
104+
if (!ThenRng)
105+
return llvm::createStringError(
106+
llvm::inconvertibleErrorCode(),
107+
"Could not obtain range of the 'then' branch. Macros?");
108+
auto ElseRng = toHalfOpenFileRange(SrcMgr, Ctx.getLangOpts(),
109+
If->getElse()->getSourceRange());
110+
if (!ElseRng)
111+
return llvm::createStringError(
112+
llvm::inconvertibleErrorCode(),
113+
"Could not obtain range of the 'else' branch. Macros?");
114+
115+
auto ThenCode = toSourceCode(SrcMgr, *ThenRng);
116+
auto ElseCode = toSourceCode(SrcMgr, *ElseRng);
117+
118+
tooling::Replacements Result;
119+
if (auto Err = Result.add(tooling::Replacement(Ctx.getSourceManager(),
120+
ThenRng->getBegin(),
121+
ThenCode.size(), ElseCode)))
122+
return std::move(Err);
123+
if (auto Err = Result.add(tooling::Replacement(Ctx.getSourceManager(),
124+
ElseRng->getBegin(),
125+
ElseCode.size(), ThenCode)))
126+
return std::move(Err);
127+
return Result;
128+
}
129+
130+
std::string SwapIfBranches::title() const { return "Swap if branches"; }
131+
} // namespace clangd
132+
} // namespace clang

clang-tools-extra/unittests/clangd/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ add_extra_unittest(ClangdTests
4545
TestTU.cpp
4646
ThreadingTests.cpp
4747
TraceTests.cpp
48+
TweakTests.cpp
4849
URITests.cpp
4950
XRefsTests.cpp
51+
52+
$<TARGET_OBJECTS:obj.clangDaemonTweaks>
5053
)
5154

5255
target_link_libraries(ClangdTests

0 commit comments

Comments
 (0)