diff --git a/clang/include/clang/Basic/Sanitizers.h b/clang/include/clang/Basic/Sanitizers.h --- a/clang/include/clang/Basic/Sanitizers.h +++ b/clang/include/clang/Basic/Sanitizers.h @@ -178,6 +178,10 @@ /// Returns a non-zero SanitizerMask, or \c 0 if \p Value is not known. SanitizerMask parseSanitizerValue(StringRef Value, bool AllowGroups); +/// Serialize a SanitizerSet into values for -fsanitize= or -fno-sanitize=. +void serializeSanitizerSet(SanitizerSet Set, + SmallVectorImpl &Values); + /// For each sanitizer group bit set in \p Kinds, set the bits for sanitizers /// this group enables. SanitizerMask expandSanitizerGroups(SanitizerMask Kinds); diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td --- a/clang/include/clang/Driver/Options.td +++ b/clang/include/clang/Driver/Options.td @@ -1401,8 +1401,7 @@ Flags<[CoreOption, NoXarchOption]>; def fsanitize_blacklist : Joined<["-"], "fsanitize-blacklist=">, Group, - HelpText<"Path to blacklist file for sanitizers">, - MarshallingInfoStringVector>; + HelpText<"Path to blacklist file for sanitizers">; def fsanitize_system_blacklist : Joined<["-"], "fsanitize-system-blacklist=">, HelpText<"Path to system blacklist file for sanitizers">, Flags<[CC1Option]>; @@ -2137,8 +2136,7 @@ def fobjc_sender_dependent_dispatch : Flag<["-"], "fobjc-sender-dependent-dispatch">, Group; def fomit_frame_pointer : Flag<["-"], "fomit-frame-pointer">, Group; def fopenmp : Flag<["-"], "fopenmp">, Group, Flags<[CC1Option, NoArgumentUnused]>, - HelpText<"Parse OpenMP pragmas and generate parallel code.">, - MarshallingInfoFlag, "0u">, Normalizer<"makeFlagToValueNormalizer(50u)">; + HelpText<"Parse OpenMP pragmas and generate parallel code.">; def fno_openmp : Flag<["-"], "fno-openmp">, Group, Flags<[NoArgumentUnused]>; def fopenmp_version_EQ : Joined<["-"], "fopenmp-version=">, Group, Flags<[CC1Option, NoArgumentUnused]>; def fopenmp_EQ : Joined<["-"], "fopenmp=">, Group; diff --git a/clang/include/clang/Frontend/CompilerInvocation.h b/clang/include/clang/Frontend/CompilerInvocation.h --- a/clang/include/clang/Frontend/CompilerInvocation.h +++ b/clang/include/clang/Frontend/CompilerInvocation.h @@ -249,11 +249,22 @@ DiagnosticsEngine &Diags); /// Parse command line options that map to LangOptions. - static bool ParseLangArgs(LangOptions &Opts, llvm::opt::ArgList &Args, - InputKind IK, const llvm::Triple &T, + static bool ParseLangArgsImpl(LangOptions &Opts, llvm::opt::ArgList &Args, + InputKind IK, const llvm::Triple &T, + std::vector &Includes, + DiagnosticsEngine &Diags); + + static bool ParseLangArgs(CompilerInvocation &Res, LangOptions &Opts, + llvm::opt::ArgList &Args, InputKind IK, + const llvm::Triple &T, std::vector &Includes, DiagnosticsEngine &Diags); + /// Generate command line options from LangOptions. + static void GenerateLangArgs(const LangOptions &Opts, + SmallVectorImpl &Args, + StringAllocator SA, const llvm::Triple &T); + /// Parse command line options that map to CodeGenOptions. static bool ParseCodeGenArgs(CodeGenOptions &Opts, llvm::opt::ArgList &Args, InputKind IK, DiagnosticsEngine &Diags, diff --git a/clang/lib/Basic/Sanitizers.cpp b/clang/lib/Basic/Sanitizers.cpp --- a/clang/lib/Basic/Sanitizers.cpp +++ b/clang/lib/Basic/Sanitizers.cpp @@ -12,6 +12,7 @@ #include "clang/Basic/Sanitizers.h" #include "llvm/ADT/Hashing.h" +#include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringSwitch.h" using namespace clang; @@ -34,6 +35,14 @@ return ParsedKind; } +void clang::serializeSanitizerSet(SanitizerSet Set, + SmallVectorImpl &Values) { +#define SANITIZER(NAME, ID) \ + if (Set.has(SanitizerKind::ID)) \ + Values.push_back(NAME); +#include "clang/Basic/Sanitizers.def" +} + SanitizerMask clang::expandSanitizerGroups(SanitizerMask Kinds) { #define SANITIZER(NAME, ID) #define SANITIZER_GROUP(NAME, ID, ALIAS) \ diff --git a/clang/lib/Frontend/CompilerInvocation.cpp b/clang/lib/Frontend/CompilerInvocation.cpp --- a/clang/lib/Frontend/CompilerInvocation.cpp +++ b/clang/lib/Frontend/CompilerInvocation.cpp @@ -214,6 +214,7 @@ Args.push_back(SA(Value)); break; case Option::JoinedClass: + case Option::CommaJoinedClass: Args.push_back(SA(Twine(Spelling) + Value)); break; default: @@ -1207,6 +1208,12 @@ } } +static SmallVector serializeSanitizerKinds(SanitizerSet S) { + SmallVector Values; + serializeSanitizerSet(S, Values); + return Values; +} + static void parseXRayInstrumentationBundle(StringRef FlagName, StringRef Bundle, ArgList &Args, DiagnosticsEngine &D, XRayInstrSet &S) { @@ -2628,19 +2635,231 @@ llvm_unreachable("unknown input language"); } -static void GenerateLangArgs(const LangOptions &Opts, - SmallVectorImpl &Args, - CompilerInvocation::StringAllocator SA) { +void CompilerInvocation::GenerateLangArgs(const LangOptions &Opts, + SmallVectorImpl &Args, + StringAllocator SA, + const llvm::Triple &T) { + OptSpecifier StdOpt; + switch (Opts.LangStd) { + case LangStandard::lang_opencl10: + case LangStandard::lang_opencl11: + case LangStandard::lang_opencl12: + case LangStandard::lang_opencl20: + case LangStandard::lang_opencl30: + case LangStandard::lang_openclcpp: + StdOpt = OPT_cl_std_EQ; + break; + default: + StdOpt = OPT_std_EQ; + break; + } + + auto LangStandard = LangStandard::getLangStandardForKind(Opts.LangStd); + GenerateArg(Args, StdOpt, LangStandard.getName(), SA); + if (Opts.IncludeDefaultHeader) GenerateArg(Args, OPT_finclude_default_header, SA); if (Opts.DeclareOpenCLBuiltins) GenerateArg(Args, OPT_fdeclare_opencl_builtins, SA); + + const LangOptions *LangOpts = &Opts; + +#define LANG_OPTION_WITH_MARSHALLING( \ + PREFIX_TYPE, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, \ + HELPTEXT, METAVAR, VALUES, SPELLING, SHOULD_PARSE, ALWAYS_EMIT, KEYPATH, \ + DEFAULT_VALUE, IMPLIED_CHECK, IMPLIED_VALUE, NORMALIZER, DENORMALIZER, \ + MERGER, EXTRACTOR, TABLE_INDEX) \ + GENERATE_OPTION_WITH_MARSHALLING( \ + Args, SA, KIND, FLAGS, SPELLING, ALWAYS_EMIT, KEYPATH, DEFAULT_VALUE, \ + IMPLIED_CHECK, IMPLIED_VALUE, DENORMALIZER, EXTRACTOR, TABLE_INDEX) +#include "clang/Driver/Options.inc" +#undef LANG_OPTION_WITH_MARSHALLING + + // The '-fcf-protection=' option is generated by CodeGenOpts generator. + + if (Opts.ObjC) { + std::string Buffer; + llvm::raw_string_ostream Stream(Buffer); + Stream << Opts.ObjCRuntime; + GenerateArg(Args, OPT_fobjc_runtime_EQ, Stream.str(), SA); + + if (Opts.GC == LangOptions::GCOnly) + GenerateArg(Args, OPT_fobjc_gc_only, SA); + else if (Opts.GC == LangOptions::HybridGC) + GenerateArg(Args, OPT_fobjc_gc, SA); + else if (Opts.ObjCAutoRefCount == 1) + GenerateArg(Args, OPT_fobjc_arc, SA); + + if (Opts.ObjCWeakRuntime) + GenerateArg(Args, OPT_fobjc_runtime_has_weak, SA); + + if (Opts.ObjCWeak) + GenerateArg(Args, OPT_fobjc_weak, SA); + + if (Opts.ObjCSubscriptingLegacyRuntime) + GenerateArg(Args, OPT_fobjc_subscripting_legacy_runtime, SA); + } + + if (Opts.GNUCVersion != 0) { + unsigned Major = Opts.GNUCVersion / 100 / 100; + unsigned Minor = (Opts.GNUCVersion / 100) % 100; + unsigned Patch = Opts.GNUCVersion % 100; + GenerateArg(Args, OPT_fgnuc_version_EQ, + Twine(Major) + "." + Twine(Minor) + "." + Twine(Patch), SA); + } + + if (Opts.SignedOverflowBehavior == LangOptions::SOB_Trapping) { + GenerateArg(Args, OPT_ftrapv, SA); + GenerateArg(Args, OPT_ftrapv_handler, Opts.OverflowHandler, SA); + } else if (Opts.SignedOverflowBehavior == LangOptions::SOB_Defined) { + GenerateArg(Args, OPT_fwrapv, SA); + } + + if (Opts.MSCompatibilityVersion != 0) { + unsigned Major = Opts.MSCompatibilityVersion / 10000000; + unsigned Minor = (Opts.MSCompatibilityVersion / 100000) % 100; + unsigned Subminor = Opts.MSCompatibilityVersion % 100000; + GenerateArg(Args, OPT_fms_compatibility_version, + Twine(Major) + "." + Twine(Minor) + "." + Twine(Subminor), SA); + } + + if ((!Opts.GNUMode && !Opts.MSVCCompat && !Opts.CPlusPlus17) || T.isOSzOS()) { + if (!Opts.Trigraphs) + GenerateArg(Args, OPT_fno_trigraphs, SA); + } else { + if (Opts.Trigraphs) + GenerateArg(Args, OPT_ftrigraphs, SA); + } + + if (Opts.Blocks && !(Opts.OpenCL && Opts.OpenCLVersion == 200)) + GenerateArg(Args, OPT_fblocks, SA); + + if (Opts.ConvergentFunctions && + !(Opts.OpenCL || (Opts.CUDA && Opts.CUDAIsDevice) || Opts.SYCLIsDevice)) + GenerateArg(Args, OPT_fconvergent_functions, SA); + + if (Opts.NoBuiltin && !Opts.Freestanding) + GenerateArg(Args, OPT_fno_builtin, SA); + + // Not generating '-fno-builtin-xxx'. It's handled for CodeGenOptions, that + // also read OPT_fno_builtin_. + + if (Opts.LongDoubleSize == 128) + GenerateArg(Args, OPT_mlong_double_128, SA); + else if (Opts.LongDoubleSize == 64) + GenerateArg(Args, OPT_mlong_double_64, SA); + + // Not generating '-mrtd', it's just an alias for '-fdefault-calling-conv='. + + // OpenMP was requested via '-fopenmp', not implied by '-fopenmp-simd' or + // '-fopenmp-targets='. + if (Opts.OpenMP && !Opts.OpenMPSimd) { + GenerateArg(Args, OPT_fopenmp, SA); + + if (Opts.OpenMP != 50) + GenerateArg(Args, OPT_fopenmp_version_EQ, Twine(Opts.OpenMP), SA); + + if (!Opts.OpenMPUseTLS) + GenerateArg(Args, OPT_fnoopenmp_use_tls, SA); + + if (Opts.OpenMPIsDevice) + GenerateArg(Args, OPT_fopenmp_is_device, SA); + + if (Opts.OpenMPIRBuilder) + GenerateArg(Args, OPT_fopenmp_enable_irbuilder, SA); + } + + if (Opts.OpenMPSimd) { + GenerateArg(Args, OPT_fopenmp_simd, SA); + + if (Opts.OpenMP != 50) + GenerateArg(Args, OPT_fopenmp_version_EQ, Twine(Opts.OpenMP), SA); + } + + if (Opts.OpenMPCUDANumSMs != 0) + GenerateArg(Args, OPT_fopenmp_cuda_number_of_sm_EQ, + Twine(Opts.OpenMPCUDANumSMs), SA); + + if (Opts.OpenMPCUDABlocksPerSM != 0) + GenerateArg(Args, OPT_fopenmp_cuda_blocks_per_sm_EQ, + Twine(Opts.OpenMPCUDABlocksPerSM), SA); + + if (Opts.OpenMPCUDAReductionBufNum != 1024) + GenerateArg(Args, OPT_fopenmp_cuda_teams_reduction_recs_num_EQ, + Twine(Opts.OpenMPCUDAReductionBufNum), SA); + + if (!Opts.OMPTargetTriples.empty()) { + std::string Targets; + llvm::raw_string_ostream OS(Targets); + llvm::interleave( + Opts.OMPTargetTriples, OS, + [&OS](const llvm::Triple &T) { OS << T.str(); }, ","); + GenerateArg(Args, OPT_fopenmp_targets_EQ, OS.str(), SA); + } + + if (!Opts.OMPHostIRFile.empty()) + GenerateArg(Args, OPT_fopenmp_host_ir_file_path, Opts.OMPHostIRFile, SA); + + if (Opts.OpenMPCUDAMode) + GenerateArg(Args, OPT_fopenmp_cuda_mode, SA); + + if (Opts.OpenMPCUDATargetParallel) + GenerateArg(Args, OPT_fopenmp_cuda_parallel_target_regions, SA); + + if (Opts.OpenMPCUDAForceFullRuntime) + GenerateArg(Args, OPT_fopenmp_cuda_force_full_runtime, SA); + + // The arguments used to set 'Optimize' and 'OptimizeSize' will be generated + // by CodeGenOptions. + + if (Opts.NoInlineDefine && Opts.Optimize) + GenerateArg(Args, OPT_fno_inline, SA); + + if (Opts.DefaultFPContractMode == LangOptions::FPM_Fast) + GenerateArg(Args, OPT_ffp_contract, "fast", SA); + else if (Opts.DefaultFPContractMode == LangOptions::FPM_On) + GenerateArg(Args, OPT_ffp_contract, "on", SA); + else if (Opts.DefaultFPContractMode == LangOptions::FPM_Off) + GenerateArg(Args, OPT_ffp_contract, "off", SA); + else if (Opts.DefaultFPContractMode == LangOptions::FPM_FastHonorPragmas) + GenerateArg(Args, OPT_ffp_contract, "fast-honor-pragmas", SA); + + for (StringRef Sanitizer : serializeSanitizerKinds(Opts.Sanitize)) + GenerateArg(Args, OPT_fsanitize_EQ, Sanitizer, SA); + + // Conflating '-fsanitize-system-blacklist' and '-fsanitize-blacklist'. + for (const std::string &F : Opts.SanitizerBlacklistFiles) + GenerateArg(Args, OPT_fsanitize_blacklist, F, SA); + + if (Opts.getClangABICompat() == LangOptions::ClangABI::Ver3_8) + GenerateArg(Args, OPT_fclang_abi_compat_EQ, "3.8", SA); + else if (Opts.getClangABICompat() == LangOptions::ClangABI::Ver4) + GenerateArg(Args, OPT_fclang_abi_compat_EQ, "4.0", SA); + else if (Opts.getClangABICompat() == LangOptions::ClangABI::Ver6) + GenerateArg(Args, OPT_fclang_abi_compat_EQ, "6.0", SA); + else if (Opts.getClangABICompat() == LangOptions::ClangABI::Ver7) + GenerateArg(Args, OPT_fclang_abi_compat_EQ, "7.0", SA); + else if (Opts.getClangABICompat() == LangOptions::ClangABI::Ver9) + GenerateArg(Args, OPT_fclang_abi_compat_EQ, "9.0", SA); + else if (Opts.getClangABICompat() == LangOptions::ClangABI::Ver11) + GenerateArg(Args, OPT_fclang_abi_compat_EQ, "11.0", SA); + + if (Opts.getSignReturnAddressScope() == + LangOptions::SignReturnAddressScopeKind::All) + GenerateArg(Args, OPT_msign_return_address_EQ, "all", SA); + else if (Opts.getSignReturnAddressScope() == + LangOptions::SignReturnAddressScopeKind::NonLeaf) + GenerateArg(Args, OPT_msign_return_address_EQ, "non-leaf", SA); + + if (Opts.getSignReturnAddressKey() == + LangOptions::SignReturnAddressKeyKind::BKey) + GenerateArg(Args, OPT_msign_return_address_key_EQ, "b_key", SA); } -bool CompilerInvocation::ParseLangArgs(LangOptions &Opts, ArgList &Args, - InputKind IK, const llvm::Triple &T, - std::vector &Includes, - DiagnosticsEngine &Diags) { +bool CompilerInvocation::ParseLangArgsImpl(LangOptions &Opts, ArgList &Args, + InputKind IK, const llvm::Triple &T, + std::vector &Includes, + DiagnosticsEngine &Diags) { unsigned NumErrorsBefore = Diags.getNumErrors(); // FIXME: Cleanup per-file based stuff. @@ -2858,6 +3077,8 @@ } } + // Check if -fopenmp is specified and set default version to 5.0. + Opts.OpenMP = Args.hasArg(OPT_fopenmp) ? 50 : 0; // Check if -fopenmp-simd is specified. bool IsSimdSpecified = Args.hasFlag(options::OPT_fopenmp_simd, options::OPT_fno_openmp_simd, @@ -2999,6 +3220,7 @@ // Parse -fsanitize= arguments. parseSanitizerKinds("-fsanitize=", Args.getAllArgValues(OPT_fsanitize_EQ), Diags, Opts.Sanitize); + Opts.SanitizerBlacklistFiles = Args.getAllArgValues(OPT_fsanitize_blacklist); std::vector systemBlacklists = Args.getAllArgValues(OPT_fsanitize_system_blacklist); Opts.SanitizerBlacklistFiles.insert(Opts.SanitizerBlacklistFiles.end(), @@ -3075,6 +3297,55 @@ return Success && Diags.getNumErrors() == NumErrorsBefore; } +bool CompilerInvocation::ParseLangArgs(CompilerInvocation &Res, + LangOptions &Opts, + llvm::opt::ArgList &Args, InputKind IK, + const llvm::Triple &T, + std::vector &Includes, + DiagnosticsEngine &Diags) { + auto DummyOpts = std::make_shared(); + + // We need to work around inconsistencies related to optimization flags. Their + // primary consumer is CodeGenOptions. However, the LangOptions parser also + // queries them, which means RoundTrip expects us to generate them. We don't + // want to do it in GenerateLangArgs, because it should eventually be the + // responsibility of GenerateCodeGenArgs. Until we start doing one big + // round-trip, let's do it here. + // + // Our parser always queries OPT_O_Group. When given -O1, -O2 or -O3, it also + // queries OPT_O. To ensure RoundTrip consistently considers us responsible + // for generating all of them, we ensure to proactively query them all. + + return RoundTrip( + [IK, &T, &Includes](CompilerInvocation &Res, ArgList &Args, + DiagnosticsEngine &Diags) { + // Proactively query all optimization flags. + Args.getLastArg(OPT_O0, OPT_O4, OPT_O, OPT_Ofast); + return ParseLangArgsImpl(*Res.getLangOpts(), Args, IK, T, Includes, + Diags); + }, + [&T, &Args](CompilerInvocation &Res, + SmallVectorImpl &GenArgs, StringAllocator SA) { + GenerateLangArgs(*Res.getLangOpts(), GenArgs, SA, T); + // Generate all optimization flags we queried. + if (Arg *A = Args.getLastArg(OPT_O_Group)) { + OptSpecifier Opt = A->getOption().getID(); + + if (A->getNumValues() > 0) + GenerateArg(GenArgs, Opt, A->getValues().back(), SA); + else + GenerateArg(GenArgs, Opt, SA); + } + + // We also queried -fcf-protection, but don't have enough information to + // generate it. Eventually, it will be generated from CodeGenOptions. + if (const Arg *A = Args.getLastArg(OPT_fcf_protection_EQ)) + GenerateArg(GenArgs, OPT_fcf_protection_EQ, A->getValue(), SA); + }, + [&DummyOpts](CompilerInvocation &Res) { Res.LangOpts.swap(DummyOpts); }, + Res, Args, Diags, "LangOptions"); +} + static bool isStrictlyPreprocessorAction(frontend::ActionKind Action) { switch (Action) { case frontend::ASTDeclList: @@ -3413,7 +3684,7 @@ } else { // Other LangOpts are only initialized when the input is not AST or LLVM IR. // FIXME: Should we really be calling this for an Language::Asm input? - Success &= ParseLangArgs(LangOpts, Args, DashX, T, + Success &= ParseLangArgs(Res, LangOpts, Args, DashX, T, Res.getPreprocessorOpts().Includes, Diags); if (Res.getFrontendOpts().ProgramAction == frontend::RewriteObjC) LangOpts.ObjCExceptions = 1; @@ -3600,21 +3871,21 @@ EXTRACTOR, TABLE_INDEX) #define DIAG_OPTION_WITH_MARSHALLING OPTION_WITH_MARSHALLING -#define LANG_OPTION_WITH_MARSHALLING OPTION_WITH_MARSHALLING #define CODEGEN_OPTION_WITH_MARSHALLING OPTION_WITH_MARSHALLING #include "clang/Driver/Options.inc" #undef CODEGEN_OPTION_WITH_MARSHALLING -#undef LANG_OPTION_WITH_MARSHALLING #undef DIAG_OPTION_WITH_MARSHALLING #undef OPTION_WITH_MARSHALLING + llvm::Triple T(TargetOpts->Triple); + GeneratePreprocessorArgs(*PreprocessorOpts, Args, SA, *LangOpts, FrontendOpts, CodeGenOpts); GenerateAnalyzerArgs(*AnalyzerOpts, Args, SA); GenerateHeaderSearchArgs(*HeaderSearchOpts, Args, SA); - GenerateLangArgs(*LangOpts, Args, SA); + GenerateLangArgs(*LangOpts, Args, SA, T); } IntrusiveRefCntPtr diff --git a/clang/unittests/Basic/CMakeLists.txt b/clang/unittests/Basic/CMakeLists.txt --- a/clang/unittests/Basic/CMakeLists.txt +++ b/clang/unittests/Basic/CMakeLists.txt @@ -8,6 +8,7 @@ FileEntryTest.cpp FileManagerTest.cpp LineOffsetMappingTest.cpp + SanitizersTest.cpp SourceManagerTest.cpp ) diff --git a/clang/unittests/Basic/SanitizersTest.cpp b/clang/unittests/Basic/SanitizersTest.cpp new file mode 100644 --- /dev/null +++ b/clang/unittests/Basic/SanitizersTest.cpp @@ -0,0 +1,49 @@ +//===- unittests/Basic/SanitizersTest.cpp - Test Sanitizers ---------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/Basic/Sanitizers.h" + +#include "gmock/gmock-matchers.h" +#include "gtest/gtest.h" + +using namespace clang; + +using testing::Contains; +using testing::Not; + +TEST(SanitizersTest, serializeSanitizers) { + SanitizerSet Set; + Set.set(parseSanitizerValue("memory", false), true); + Set.set(parseSanitizerValue("nullability-arg", false), true); + + SmallVector Serialized; + serializeSanitizerSet(Set, Serialized); + + ASSERT_EQ(Serialized.size(), 2u); + ASSERT_THAT(Serialized, Contains("memory")); + ASSERT_THAT(Serialized, Contains("nullability-arg")); +} + +TEST(SanitizersTest, serializeSanitizersIndividual) { + SanitizerSet Set; + Set.set(parseSanitizerValue("memory", false), true); + Set.set(parseSanitizerValue("nullability-arg", false), true); + Set.set(parseSanitizerValue("nullability-assign", false), true); + Set.set(parseSanitizerValue("nullability-return", false), true); + + SmallVector Serialized; + serializeSanitizerSet(Set, Serialized); + + ASSERT_EQ(Serialized.size(), 4u); + ASSERT_THAT(Serialized, Contains("memory")); + ASSERT_THAT(Serialized, Contains("nullability-arg")); + ASSERT_THAT(Serialized, Contains("nullability-assign")); + ASSERT_THAT(Serialized, Contains("nullability-return")); + // Individual sanitizers don't get squashed into a single group. + ASSERT_THAT(Serialized, Not(Contains("nullability"))); +}