Skip to content

Commit 37d50ee

Browse files
committed
Add clang-tidy plugin for modernize-nlohmann-json-explicit-conversions
Add a clang-tidy plugin containing one check to replace implicit conversions (as enabled by default with JSON_USE_IMPLICIT_CONVERSIONS) with explicit ones. This will make it easier for library users to switch away from using implicit conversions which should make it possible for the library to start disallowing them sooner. Being able to test the plugin in a similar way to how checks are tested within clang-tidy itself requires copying the check_clang_tidy.py script from the LLVM code itself. The check itself is virtually identical to the one proposed for inclusion in clang-tidy itself at llvm/llvm-project#126425 . Unfortunately it is necessary to add "C" to the languages for the project in CMakeLists.txt for find_package to work for LLVM. Signed-off-by: Mike Crowe <[email protected]>
1 parent 8215dba commit 37d50ee

File tree

11 files changed

+907
-1
lines changed

11 files changed

+907
-1
lines changed

.reuse/dep5

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ License: BSD-3-Clause
3434
Files: tools/gdb_pretty_printer/*
3535
Copyright: 2020 Hannes Domani <https://github.com/ssbssa>
3636
License: MIT
37+
38+
Files: tests/clang_tidy_plugin/check_clang_tidy.py
39+
Copyright: 2003-2019 University of Illinois at Urbana-Champaign.
40+
License: Apache-2.0 WITH LLVM-exception

CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.1...3.14)
44
## PROJECT
55
## name and version
66
##
7-
project(nlohmann_json VERSION 3.11.3 LANGUAGES CXX)
7+
project(nlohmann_json VERSION 3.11.3 LANGUAGES C CXX)
88

99
##
1010
## MAIN_PROJECT CHECK
@@ -48,6 +48,7 @@ option(JSON_LegacyDiscardedValueComparison "Enable legacy discarded value compar
4848
option(JSON_Install "Install CMake targets during install step." ${MAIN_PROJECT})
4949
option(JSON_MultipleHeaders "Use non-amalgamated version of the library." ON)
5050
option(JSON_SystemInclude "Include as system headers (skip for clang-tidy)." OFF)
51+
option(JSON_ClangTidyPlugin "Build clang-tidy plug-in." OFF)
5152

5253
if (JSON_CI)
5354
include(ci)
@@ -158,6 +159,12 @@ CONFIGURE_FILE(
158159
@ONLY
159160
)
160161

162+
if (JSON_ClangTidyPlugin)
163+
find_package(LLVM REQUIRED CONFIG)
164+
find_package(Clang REQUIRED CONFIG)
165+
add_subdirectory(clang_tidy_plugin)
166+
endif()
167+
161168
##
162169
## TESTS
163170
## create and configure the unit test target

LICENSES/LLVM-exception.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---- LLVM Exceptions to the Apache 2.0 License ----
2+
3+
As an exception, if, as a result of your compiling your source code, portions
4+
of this Software are embedded into an Object form of such source code, you
5+
may redistribute such embedded portions in such Object form without complying
6+
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
7+
8+
In addition, if you combine or link compiled forms of this Software with
9+
software that is licensed under the GPLv2 ("Combined Software") and if a
10+
court of competent jurisdiction determines that the patent provision (Section
11+
3), the indemnity provision (Section 9) or other Section of the License
12+
conflicts with the conditions of the GPLv2, you may retroactively and
13+
prospectively choose to deem waived or otherwise exclude such Section(s) of
14+
the License, but only in their entirety and only with respect to the Combined
15+
Software.

clang_tidy_plugin/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
add_library(NlohmannJsonClangTidyPlugin MODULE "")
2+
target_compile_options(NlohmannJsonClangTidyPlugin PRIVATE -fno-rtti)
3+
target_include_directories(NlohmannJsonClangTidyPlugin
4+
PRIVATE
5+
${CLANG_INCLUDE_DIRS}
6+
${LLVM_INCLUDE_DIRS}
7+
)
8+
target_sources(NlohmannJsonClangTidyPlugin PRIVATE
9+
${CMAKE_CURRENT_LIST_DIR}/Module.cpp
10+
${CMAKE_CURRENT_LIST_DIR}/ModernizeNlohmannJsonExplicitConversionsCheck.cpp
11+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-FileCopyrightText: 2025 Mike Crowe
2+
// SPDX-License-Identifier: MIT
3+
4+
#include "ModernizeNlohmannJsonExplicitConversionsCheck.h"
5+
#include "clang/ASTMatchers/ASTMatchFinder.h"
6+
#include "clang/Lex/Lexer.h"
7+
8+
using namespace clang::ast_matchers;
9+
10+
namespace {
11+
void replaceAll(std::string &Str, const std::string &From, const std::string &To) {
12+
assert(!From.empty());
13+
14+
std::string::size_type StartPos = 0;
15+
while ((StartPos = Str.find(from, StartPos)) != std::string::npos) {
16+
Str.replace(StartPos, From.length(), To);
17+
StartPos += To.length();
18+
}
19+
}
20+
}
21+
22+
namespace clang::tidy::modernize {
23+
24+
void NlohmannJsonExplicitConversionsCheck::registerMatchers(
25+
MatchFinder *Finder) {
26+
auto Matcher =
27+
cxxMemberCallExpr(
28+
on(expr().bind("arg")),
29+
callee(cxxConversionDecl(ofClass(hasName("nlohmann::basic_json")))
30+
.bind("conversionDecl")))
31+
.bind("conversionCall");
32+
Finder->addMatcher(Matcher, this);
33+
}
34+
35+
void NlohmannJsonExplicitConversionsCheck::check(
36+
const MatchFinder::MatchResult &Result) {
37+
const auto *Decl =
38+
Result.Nodes.getNodeAs<CXXConversionDecl>("conversionDecl");
39+
const auto *Call =
40+
Result.Nodes.getNodeAs<CXXMemberCallExpr>("conversionCall");
41+
const auto *Arg = Result.Nodes.getNodeAs<Expr>("arg");
42+
43+
const QualType DestinationType = Decl->getConversionType();
44+
std::string DestinationTypeStr =
45+
DestinationType.getAsString(Result.Context->getPrintingPolicy());
46+
replaceAll(DestinationTypeStr, "std::basic_string<char>", "std::string");
47+
48+
const SourceRange ExprRange = Call->getSourceRange();
49+
if (!ExprRange.isValid())
50+
return;
51+
52+
bool Deref = false;
53+
if (const auto *Op = llvm::dyn_cast<UnaryOperator>(Arg);
54+
Op && Op->getOpcode() == UO_Deref)
55+
Deref = true;
56+
else if (const auto *Op = llvm::dyn_cast<CXXOperatorCallExpr>(Arg);
57+
Op && Op->getOperator() == OO_Star)
58+
Deref = true;
59+
60+
llvm::StringRef SourceText = clang::Lexer::getSourceText(
61+
clang::CharSourceRange::getTokenRange(ExprRange), *Result.SourceManager,
62+
Result.Context->getLangOpts());
63+
64+
if (Deref)
65+
SourceText.consume_front("*");
66+
67+
const std::string ReplacementText =
68+
(llvm::Twine(SourceText) + (Deref ? "->" : ".") + "get<" +
69+
DestinationTypeStr + ">()")
70+
.str();
71+
diag(Call->getExprLoc(),
72+
"implicit nlohmann::json conversion to %0 should be explicit")
73+
<< DestinationTypeStr
74+
<< FixItHint::CreateReplacement(CharSourceRange::getTokenRange(ExprRange),
75+
ReplacementText);
76+
}
77+
78+
} // namespace clang::tidy::modernize
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: 2025 Mike Crowe
2+
// SPDX-License-Identifier: MIT
3+
4+
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_NLOHMANNJSONEXPLICITCONVERSIONSCHECK_H
5+
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_NLOHMANNJSONEXPLICITCONVERSIONSCHECK_H
6+
7+
#include "clang-tidy/ClangTidyCheck.h"
8+
9+
namespace clang::tidy::modernize {
10+
11+
/// Convert implicit conversions via operator ValueType() from nlohmann::json to
12+
/// explicit calls to the get<> method because the next major version of the
13+
/// library will remove support for implicit conversions.
14+
///
15+
/// For the user-facing documentation see:
16+
/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/nlohmann-json-explicit-conversions.html
17+
class NlohmannJsonExplicitConversionsCheck : public ClangTidyCheck {
18+
public:
19+
NlohmannJsonExplicitConversionsCheck(StringRef Name,
20+
ClangTidyContext *Context)
21+
: ClangTidyCheck(Name, Context) {}
22+
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
23+
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
24+
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
25+
return LangOpts.CPlusPlus;
26+
}
27+
};
28+
29+
} // namespace clang::tidy::modernize
30+
31+
#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_NLOHMANNJSONEXPLICITCONVERSIONSCHECK_H

clang_tidy_plugin/Module.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: 2025 Mike Crowe
2+
// SPDX-License-Identifier: MIT
3+
4+
#include "clang-tidy/ClangTidyModule.h"
5+
#include "clang-tidy/ClangTidyModuleRegistry.h"
6+
#include "ModernizeNlohmannJsonExplicitConversionsCheck.h"
7+
8+
namespace {
9+
10+
using namespace clang::tidy;
11+
12+
class NlohmannJsonChecksModule : public ClangTidyModule
13+
{
14+
public:
15+
void addCheckFactories(ClangTidyCheckFactories& CheckFactories) override
16+
{
17+
CheckFactories.registerCheck<clang::tidy::modernize::NlohmannJsonExplicitConversionsCheck>(
18+
"modernize-nlohmann-json-explicit-conversions");
19+
}
20+
};
21+
22+
} // namespace
23+
24+
namespace clang::tidy {
25+
26+
// Register the module using this statically initialized variable.
27+
static ClangTidyModuleRegistry::Add<::NlohmannJsonChecksModule> nlohmannJsonChecksInit(
28+
"nlohmann-json-checks-module",
29+
"Adds 'modernize-nlohmann-json-explicit-conversions' check.");
30+
31+
} // namespace clang::tidy

tests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ json_test_add_test_for(src/unit-comparison.cpp
168168

169169
add_subdirectory(abi)
170170

171+
if (JSON_ClangTidyPlugin)
172+
add_subdirectory(clang_tidy_plugin)
173+
endif()
174+
171175
#############################################################################
172176
# Test the generated build configs
173177
#############################################################################
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
include(CTest)
2+
3+
if(NOT LLVM_TOOLS_BINARY_DIR)
4+
message(FATAL_ERROR "Could not determine LLVM binary directory")
5+
endif()
6+
7+
add_test(NAME modernize-nlohmann-json-explicit-conversions-check COMMAND ${CMAKE_COMMAND} -E env PATH=${LLVM_TOOLS_BINARY_DIR}:$ENV{PATH}
8+
${CMAKE_CURRENT_LIST_DIR}/check_clang_tidy.py -std=c++17
9+
-plugin=$<TARGET_FILE:NlohmannJsonClangTidyPlugin>
10+
${CMAKE_CURRENT_LIST_DIR}/modernize-nlohmann-json-explicit-conversions.cpp modernize-nlohmann-json-explicit-conversions temp)

0 commit comments

Comments
 (0)