Skip to content

Conversation

@DheerajAgarwal1234
Copy link

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.

@github-actions
Copy link

github-actions bot commented Nov 6, 2025

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 @ followed by their GitHub username.

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.

@llvmbot
Copy link
Member

llvmbot commented Nov 6, 2025

@llvm/pr-subscribers-clang-tidy

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

Author: None (DheerajAgarwal1234)

Changes

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.


Full diff: https://github.com/llvm/llvm-project/pull/166822.diff

5 Files Affected:

  • (modified) clang-tools-extra/CMakeLists.txt (+1)
  • (added) clang-tools-extra/clang-convert-ternary-if/CMakeLists.txt (+20)
  • (added) clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.cpp (+111)
  • (added) clang-tools-extra/clang-convert-ternary-if/ConvertTernaryIf.h (+39)
  • (added) clang-tools-extra/clang-convert-ternary-if/ToolMain.cpp (+80)
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; +} + 
@zwuis
Copy link
Contributor

zwuis commented Nov 7, 2025

Thank you for your patch! I think it would be great to make it a clang-tidy check.

@vbvictor
Copy link
Contributor

vbvictor commented Nov 7, 2025

Thank you for your patch! I think it would be great to make it a clang-tidy check.

+1
Could you explain why it shouldn't be a clang-tidy check?

@DheerajAgarwal1234
Copy link
Author

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.

@vbvictor
Copy link
Contributor

vbvictor commented Nov 7, 2025

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.

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/.

@vbvictor
Copy link
Contributor

vbvictor commented Nov 7, 2025

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.

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

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

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

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 = 
@github-actions
Copy link

github-actions bot commented Nov 7, 2025

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

@vbvictor
Copy link
Contributor

vbvictor commented Nov 7, 2025

If AI was used to create PR, please disclose it. And to what extent it was used.

@zwuis
Copy link
Contributor

zwuis commented Nov 7, 2025

FYI, https://clang.llvm.org/extra/clang-tidy/Contributing.html is useful for writing clang-tidy checks.

Copy link
Contributor

@EugeneZelenko EugeneZelenko left a 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();
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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);
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
#endif
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_CONDITIONALTOIFCHECK_H
@DheerajAgarwal1234
Copy link
Author

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.

@EugeneZelenko
Copy link
Contributor

modernize module is about upgrading to features of new C++/C standards. Ternary and conditional operator are part of original C. Choice between them is clearly question of code style.

@DheerajAgarwal1234
Copy link
Author

That’s a good observation, I went with modernize mainly because the last round of feedback suggested following the transformation-based checks model.
Looks like whichever folder I choose, I’ll still manage to be in the wrong one

@vbvictor
Copy link
Contributor

vbvictor commented Nov 7, 2025

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
Copy link
Contributor

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(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
std::string CondStr = Lexer::getSourceText(
const std::string CondStr = Lexer::getSourceText(

Same below.

}

} // namespace clang::tidy::readability

Copy link
Contributor

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++ -*-===//
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
//===--- ConditionalToIfCheck.cpp - clang-tidy -------------------*- C++ -*-===//
//===----------------------------------------------------------------------===//
@@ -0,0 +1,35 @@
//===--- ConditionalToIfCheck.h - clang-tidy ---------------------*- C++ -*-===//
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
//===--- ConditionalToIfCheck.h - clang-tidy ---------------------*- C++ -*-===//
//===----------------------------------------------------------------------===//
#include "UseAnyOfAllOfCheck.h"
#include "UseConcisePreprocessorDirectivesCheck.h"
#include "UseStdMinMaxCheck.h"
#include "ConditionalToIfCheck.h"
Copy link
Contributor

Choose a reason for hiding this comment

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

Alphabetical order, please.

Comment on lines 188 to 189
CheckFactories.registerCheck<ConditionalToIfCheck>(
"readability-conditional-to-if");
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

Comment on lines 1 to 2
//===--- ConditionalToIfCheck.cpp - clang-tidy -------------------*- C++
//-*-===//
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
//===--- 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(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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.

.str();

// Construct the replacement code
std::string Replacement =
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
std::string Replacement =
const std::string Replacement =
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment