- Notifications
You must be signed in to change notification settings - Fork 15.1k
Add refactoring tool to convert between ternary (?:) expressions and equivalent if/else statements #166822
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
base: main
Are you sure you want to change the base?
Conversation
| Thank you for submitting a Pull Request (PR) to the LLVM Project! This PR will be automatically labeled and the relevant teams will be notified. If you wish to, you can add reviewers by using the "Reviewers" section on this page. If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers. If you have further questions, they may be answered by the LLVM GitHub User Guide. You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums. |
| @llvm/pr-subscribers-clang-tidy @llvm/pr-subscribers-clang-tools-extra Author: None (DheerajAgarwal1234) ChangesThis patch introduces a new standalone refactoring tool under clang-tools-extra named clang-convert-ternary-if. The implementation uses Clang’s AST Matchers to detect conditional operators and if statements, and the Rewriter API to perform precise source-level transformations. Full diff: https://github.com/llvm/llvm-project/pull/166822.diff 5 Files Affected:
diff --git a/clang-tools-extra/CMakeLists.txt b/clang-tools-extra/CMakeLists.txt index 87050db4e0e75..c0e6888b6209a 100644 --- a/clang-tools-extra/CMakeLists.txt +++ b/clang-tools-extra/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory(clang-apply-replacements) add_subdirectory(clang-reorder-fields) add_subdirectory(modularize) add_subdirectory(clang-tidy) +add_subdirectory(clang-convert-ternary-if) add_subdirectory(clang-change-namespace) add_subdirectory(clang-doc) diff --git a/clang-tools-extra/clang-convert-ternary-if/CMakeLists.txt b/clang-tools-extra/clang-convert-ternary-if/CMakeLists.txt new file mode 100644 index 0000000000000..780fca4405e64 --- /dev/null +++ b/clang-tools-extra/clang-convert-ternary-if/CMakeLists.txt @@ -0,0 +1,20 @@ +#===- CMakeLists.txt - clang-convert-ternary-if tool ---------------------===# +# This defines the build configuration for the standalone refactoring tool. + +add_clang_executable(clang-convert-ternary-if + ConvertTernaryIf.cpp + ToolMain.cpp +) + +target_link_libraries(clang-convert-ternary-if + PRIVATE + clangTooling + clangBasic + clangAST + clangASTMatchers + clangRewrite + clangFrontend +) + +install(TARGETS clang-convert-ternary-if RUNTIME DESTINATION bin) + diff --git a/clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.cpp b/clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.cpp new file mode 100644 index 0000000000000..8843510d08b72 --- /dev/null +++ b/clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.cpp @@ -0,0 +1,111 @@ +//===--- ConvertTernaryIf.cpp ---------------------------------------------===// +// +// Implements a tool that refactors between ternary (?:) expressions +// and equivalent if/else statements. +// +// Usage Example: +// clang-convert-ternary-if test.cpp -- +// +//===----------------------------------------------------------------------===// + +#include "ConvertTernaryIf.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Rewrite/Core/Rewriter.h" +#include "clang/Lex/Lexer.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace clang::ast_matchers; + +namespace clang { +namespace convertternary { + + +// Callback: Called when a match is found +void ConvertTernaryIfCallback::run(const MatchFinder::MatchResult &Result) { + //Initialize the Rewriter safely (fixes segmentation fault) + if (!IsInitialized) { + if (Result.SourceManager && Result.Context) { + TheRewriter.setSourceMgr(*Result.SourceManager, + Result.Context->getLangOpts()); + IsInitialized = true; + llvm::errs() << "Rewriter initialized successfully.\n"; + } else { + llvm::errs() << "Error: Missing SourceManager or Context.\n"; + return; + } + } + + const auto *CondOp = Result.Nodes.getNodeAs<ConditionalOperator>("condOp"); + const auto *IfStmtNode = Result.Nodes.getNodeAs<IfStmt>("ifStmt"); + + const SourceManager &SM = *Result.SourceManager; + + // === Convert Ternary -> If === + if (CondOp) { + const Expr *Cond = CondOp->getCond(); + const Expr *TrueExpr = CondOp->getTrueExpr(); + const Expr *FalseExpr = CondOp->getFalseExpr(); + + auto getText = [&](const Expr *E) -> std::string { + return Lexer::getSourceText(CharSourceRange::getTokenRange(E->getSourceRange()), SM, + Result.Context->getLangOpts()) + .str(); + }; + + std::string CondText = getText(Cond); + std::string TrueText = getText(TrueExpr); + std::string FalseText = getText(FalseExpr); + + std::string IfReplacement = "if (" + CondText + ") {\n " + TrueText + + ";\n} else {\n " + FalseText + ";\n}"; + + TheRewriter.ReplaceText(CondOp->getSourceRange(), IfReplacement); + llvm::errs() << "Converted ternary to if/else.\n"; + } + + // === Convert If -> Ternary === + if (IfStmtNode) { + const Expr *Cond = IfStmtNode->getCond(); + const Stmt *Then = IfStmtNode->getThen(); + const Stmt *Else = IfStmtNode->getElse(); + + if (!Then || !Else) + return; + + auto getTextStmt = [&](const Stmt *S) -> std::string { + return Lexer::getSourceText(CharSourceRange::getTokenRange(S->getSourceRange()), SM, + Result.Context->getLangOpts()) + .str(); + }; + + std::string CondText = Lexer::getSourceText( + CharSourceRange::getTokenRange(Cond->getSourceRange()), SM, + Result.Context->getLangOpts()) + .str(); + + std::string ThenText = getTextStmt(Then); + std::string ElseText = getTextStmt(Else); + + std::string Ternary = + "(" + CondText + ") ? " + ThenText + " : " + ElseText + ";"; + + TheRewriter.ReplaceText(IfStmtNode->getSourceRange(), Ternary); + llvm::errs() << "Converted if/else to ternary.\n"; + } +} + +// === Register AST Matchers === +void setupMatchers(MatchFinder &Finder, ConvertTernaryIfCallback &Callback) { + Finder.addMatcher( + conditionalOperator(isExpansionInMainFile()).bind("condOp"), &Callback); + + Finder.addMatcher( + ifStmt(hasThen(stmt()), hasElse(stmt()), isExpansionInMainFile()) + .bind("ifStmt"), + &Callback); +} + +} // namespace convertternary +} // namespace clang + diff --git a/clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.h b/clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.h new file mode 100644 index 0000000000000..2d71cf5f8242d --- /dev/null +++ b/clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.h @@ -0,0 +1,39 @@ +//===--- ConvertTernaryIf.h ------------------------------------*- C++ -*-===// +// +// This file declares the refactoring logic that converts +// ternary operators (?:) into if/else statements and vice versa. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_CONVERT_TERNARY_IF_H +#define LLVM_CLANG_CONVERT_TERNARY_IF_H + +#include "clang/AST/AST.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/Tooling/Refactoring.h" +#include "clang/Rewrite/Core/Rewriter.h" + +namespace clang { +namespace convertternary { + +class ConvertTernaryIfCallback + : public ast_matchers::MatchFinder::MatchCallback { +public: + ConvertTernaryIfCallback(Rewriter &R) + : TheRewriter(R), IsInitialized(false) {} + + void run(const ast_matchers::MatchFinder::MatchResult &Result) override; + +private: + Rewriter &TheRewriter; + bool IsInitialized; + }; + +void setupMatchers(ast_matchers::MatchFinder &Finder, + ConvertTernaryIfCallback &Callback); + +} // namespace convertternary +} // namespace clang + +#endif + diff --git a/clang-tools-extra/clang-convert-ternary-if/ToolMain.cpp b/clang-tools-extra/clang-convert-ternary-if/ToolMain.cpp new file mode 100644 index 0000000000000..bf9a793309944 --- /dev/null +++ b/clang-tools-extra/clang-convert-ternary-if/ToolMain.cpp @@ -0,0 +1,80 @@ +//===--- ToolMain.cpp - Entry point for clang-convert-ternary-if ----------===// +// +// This tool runs the refactoring logic defined in ConvertTernaryIf.cpp. +// +// Usage: +// clang-convert-ternary-if <source-file> -- +// +// It prints the rewritten (refactored) source code to stdout. +// +//===----------------------------------------------------------------------===// + +#include "ConvertTernaryIf.h" +#include "clang/Tooling/CommonOptionsParser.h" +#include "clang/Tooling/Tooling.h" +#include "clang/Frontend/TextDiagnosticPrinter.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/raw_ostream.h" + +using namespace clang; +using namespace clang::tooling; +using namespace clang::convertternary; +using namespace llvm; + +static llvm::cl::OptionCategory ToolCategory("convert-ternary-if options"); + +int main(int argc, const char **argv) { + // Parse command-line options + auto ExpectedParser = + CommonOptionsParser::create(argc, argv, ToolCategory, cl::ZeroOrMore); + if (!ExpectedParser) { + llvm::errs() << ExpectedParser.takeError(); + return 1; + } + + CommonOptionsParser &OptionsParser = ExpectedParser.get(); + ClangTool Tool(OptionsParser.getCompilations(), + OptionsParser.getSourcePathList()); + + // Set up the Rewriter and the Matcher + clang::Rewriter Rewrite; + ast_matchers::MatchFinder Finder; + ConvertTernaryIfCallback Callback(Rewrite); + setupMatchers(Finder, Callback); + + llvm::outs() << "=== Running clang-convert-ternary-if ===\n"; + int Result = Tool.run(newFrontendActionFactory(&Finder).get()); + + if (Result != 0) { + llvm::errs() << "Error: Tool execution failed.\n"; + return Result; + } + + // No changes? + if (Rewrite.buffer_begin() == Rewrite.buffer_end()) { + llvm::outs() << "No changes made.\n"; + return 0; + } + + llvm::outs() << "\n=== Rewritten Files ===\n"; + + // Print all rewritten files + for (auto It = Rewrite.buffer_begin(); It != Rewrite.buffer_end(); ++It) { + clang::FileID FID = It->first; + const llvm::RewriteBuffer &RewriteBuf = It->second; + const clang::SourceManager &SM = Rewrite.getSourceMgr(); + + // Get the filename safely + llvm::StringRef FileName = SM.getFilename(SM.getLocForStartOfFile(FID)); + if (FileName.empty()) + FileName = "<unknown file>"; + + llvm::outs() << "\n--- " << FileName << " ---\n"; + RewriteBuf.write(llvm::outs()); + llvm::outs() << "\n"; + } + + llvm::outs() << "\n=== Refactoring complete ===\n"; + return 0; +} + |
| Thank you for your patch! I think it would be great to make it a clang-tidy check. |
+1 |
| I felt this fits better as a refactoring tool rather than a clang-tidy check, since it’s a structural transformation rather than a diagnostic. The change often involves multi-line edits, declaration restructuring, and control-flow adjustments—areas clang-tidy fix-its typically avoid to ensure safety. Refactoring tools, on the other hand, are designed for explicit, user-invoked transformations like this. |
In clang-tidy, we have "modernize" checks whose sole purpose is transform the code. Take a look at https://clang.llvm.org/extra/clang-tidy/checks/modernize/loop-convert.html which transform index-based loops to range-based. Anyway, all new top-level tools need to go through RFC process on https://discourse.llvm.org/. |
| In current state, the tool is 150-lines long which is the size of a small clang-tidy check, so I highly advise to convert it to clang-tidy. Also, add tests please. |
You can test this locally with the following command:git-clang-format --diff origin/main HEAD --extensions h,cpp -- clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.h clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp --diff_from_common_commit
View the diff from clang-format here.diff --git a/clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp b/clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp index 1946f91f0..421a0fcfd 100644 --- a/clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp +++ b/clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp @@ -62,20 +62,23 @@ void ConditionalToIfCheck::check(const MatchFinder::MatchResult &Result) { "replace ternary operator with if/else statement for readability"); // Extract source text for condition, true and false expressions - const std::string CondStr = Lexer::getSourceText(CharSourceRange::getTokenRange( - Cond->getSourceRange()), - SM, Result.Context->getLangOpts()) - .str(); + const std::string CondStr = + Lexer::getSourceText( + CharSourceRange::getTokenRange(Cond->getSourceRange()), SM, + Result.Context->getLangOpts()) + .str(); - const std::string TrueStr = Lexer::getSourceText(CharSourceRange::getTokenRange( - TrueExpr->getSourceRange()), - SM, Result.Context->getLangOpts()) - .str(); + const std::string TrueStr = + Lexer::getSourceText( + CharSourceRange::getTokenRange(TrueExpr->getSourceRange()), SM, + Result.Context->getLangOpts()) + .str(); - const std::string FalseStr = Lexer::getSourceText(CharSourceRange::getTokenRange( - FalseExpr->getSourceRange()), - SM, Result.Context->getLangOpts()) - .str(); + const std::string FalseStr = + Lexer::getSourceText( + CharSourceRange::getTokenRange(FalseExpr->getSourceRange()), SM, + Result.Context->getLangOpts()) + .str(); // Construct the replacement code const std::string Replacement = |
| ✅ With the latest revision this PR passed the C/C++ code linter. |
| If AI was used to create PR, please disclose it. And to what extent it was used. |
| FYI, https://clang.llvm.org/extra/clang-tidy/Contributing.html is useful for writing clang-tidy checks. |
EugeneZelenko left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add documentation and Release Notes entry.
I don't think that this check belongs to modernize, most likely to readability, since it's code style issue.
| hasObviousSideEffects(FalseE, Ctx)) | ||
| return; | ||
| | ||
| SourceRange SR = Ret->getSourceRange(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be const. Same below.
| hasObviousSideEffects(ElseR, Ctx)) | ||
| return; | ||
| | ||
| SourceRange SR = IfR->getSourceRange(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| if (!Carrier) | ||
| Carrier = Assign; | ||
| | ||
| SourceRange SR = Carrier->getSourceRange(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| return; | ||
| | ||
| // LHS must be textually identical (safe & simple). | ||
| std::string LThen = getText(ThenL->getSourceRange(), Res); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| /// | ||
| /// Direction is controlled by the option: | ||
| /// - modernize-conditional-to-if.PreferredForm: "if" | "conditional" | ||
| class ConditionalToIfCheck : public ClangTidyCheck { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add link to documentation. See other checks as example.
| | ||
| } // namespace clang::tidy::modernize | ||
| | ||
| #endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #endif | |
| #endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_CONDITIONALTOIFCHECK_H |
| Hi Eugene, Thanks for the feedback! I initially had this implemented as a standalone tool, but after vbvictor’s suggestion, I reworked it into a clang-tidy modernize check, following the example of other transformation-based checks like modernize-loop-convert. That’s why it currently resides under the modernize module . I wanted to align with that advice and make sure it fit the clang-tidy structure properly. I’ve got it working again in this setup. |
|
|
| That’s a good observation, I went with modernize mainly because the last round of feedback suggested following the transformation-based checks model. |
| I gave modernize category as an example that transformations can be done in clang-tidy. This to me is the readability category. To proceed further we need tests and docs for the check, see how other checks were implemented. (you can check git log and find related PRs) |
| UseAnyOfAllOfCheck.cpp | ||
| UseConcisePreprocessorDirectivesCheck.cpp | ||
| UseStdMinMaxCheck.cpp | ||
| ConditionalToIfCheck.cpp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep alphabetical order.
| "replace ternary operator with if/else statement for readability"); | ||
| | ||
| // Extract source text for condition, true and false expressions | ||
| std::string CondStr = Lexer::getSourceText( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| std::string CondStr = Lexer::getSourceText( | |
| const std::string CondStr = Lexer::getSourceText( |
Same below.
| } | ||
| | ||
| } // namespace clang::tidy::readability | ||
| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excessive newline.
| @@ -0,0 +1,85 @@ | |||
| //===--- ConditionalToIfCheck.cpp - clang-tidy -------------------*- C++ -*-===// | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| //===--- ConditionalToIfCheck.cpp - clang-tidy -------------------*- C++ -*-===// | |
| //===----------------------------------------------------------------------===// |
| @@ -0,0 +1,35 @@ | |||
| //===--- ConditionalToIfCheck.h - clang-tidy ---------------------*- C++ -*-===// | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| //===--- ConditionalToIfCheck.h - clang-tidy ---------------------*- C++ -*-===// | |
| //===----------------------------------------------------------------------===// |
| #include "UseAnyOfAllOfCheck.h" | ||
| #include "UseConcisePreprocessorDirectivesCheck.h" | ||
| #include "UseStdMinMaxCheck.h" | ||
| #include "ConditionalToIfCheck.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alphabetical order, please.
| CheckFactories.registerCheck<ConditionalToIfCheck>( | ||
| "readability-conditional-to-if"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| //===--- ConditionalToIfCheck.cpp - clang-tidy -------------------*- C++ | ||
| //-*-===// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| //===--- ConditionalToIfCheck.cpp - clang-tidy -------------------*- C++ | |
| //-*-===// | |
| //===----------------------------------------------------------------------===// |
Emacs stuff was removed (at least from Clang-Tidy code) recently. Same for other file.
| std::string CondStr = Lexer::getSourceText( | ||
| CharSourceRange::getTokenRange(Cond->getSourceRange()), SM, | ||
| Result.Context->getLangOpts()).str(); | ||
| std::string CondStr = Lexer::getSourceText(CharSourceRange::getTokenRange( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| std::string CondStr = Lexer::getSourceText(CharSourceRange::getTokenRange( | |
| const std::string CondStr = Lexer::getSourceText(CharSourceRange::getTokenRange( |
Same below. See https://clang.llvm.org/extra/clang-tidy/checks/misc/const-correctness.html.
clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.h Outdated Show resolved Hide resolved
clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp Outdated Show resolved Hide resolved
clang-tools-extra/clang-tidy/readability/ConditionalToIfCheck.cpp Outdated Show resolved Hide resolved
| .str(); | ||
| | ||
| // Construct the replacement code | ||
| std::string Replacement = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| std::string Replacement = | |
| const std::string Replacement = |
This patch introduces a new standalone refactoring tool under clang-tools-extra named clang-convert-ternary-if.
The tool automates conversion between ternary (?:) expressions and equivalent if/else statements, and vice versa.
The implementation uses Clang’s AST Matchers to detect conditional operators and if statements, and the Rewriter API to perform precise source-level transformations.
It aims to assist developers in simplifying or expanding conditional expressions based on readability or coding-style preferences.