From 35426ac778d6145253abbd47e7f13d782c4eab1b Mon Sep 17 00:00:00 2001 From: Niels Dekker Date: Thu, 23 Feb 2023 23:56:48 +0100 Subject: [PATCH] ENH: Add C++ script to add in-class `{}` member initializers to ITK The script that was used for the following pull requests: pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3851 "STYLE: Add in-class `{}` member initializers to objects created by New()" pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3913 "STYLE: Add in-class `{}` initializers to classes with virtual functions" pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3917 "STYLE: Add in-class `{}` initializers to classes with override = default" pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3941 "STYLE: Add in-class `{}` member initializers to 3-letter data members" pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3945 "STYLE: Add in-class `{}` member initializers having trailing comments" --- .../AddEmptyDefaultMemberInitializers.cxx | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 Utilities/Maintenance/AddEmptyDefaultMemberInitializers.cxx diff --git a/Utilities/Maintenance/AddEmptyDefaultMemberInitializers.cxx b/Utilities/Maintenance/AddEmptyDefaultMemberInitializers.cxx new file mode 100644 index 00000000000..7bb8ad56ed8 --- /dev/null +++ b/Utilities/Maintenance/AddEmptyDefaultMemberInitializers.cxx @@ -0,0 +1,242 @@ +/*========================================================================= + * + * Copyright NumFOCUS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *=========================================================================*/ + +// Script to add in-class {} member initializers to ITK classes that have one or more virtual member function, +// itkNewMacro, itkSimpleNewMacro, or itkCreateAnotherMacro calls. +// +// Tested with Visual Studio 2022 (C++14). +// +// Needs to have the directory path to the ITK sources as command-line argument, for example: +// +// AddEmptyDefaultMemberInitializers.exe D:\src\ITK\Modules +// +// Initial version by Niels Dekker, LKEB, Leiden University Medical Center, 2023 + +#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::experimental::filesystem::v1; + +namespace +{ +using Lines = std::deque; + +auto +ReadFile(const path & filePath) +{ + Lines result; + + std::ifstream inputFileStream{ filePath }; + std::string line; + while (std::getline(inputFileStream, line)) + { + result.push_back(line); + } + return result; +} + +void +WriteFile(const path & filePath, const Lines & lines) +{ + std::ofstream outputFileStream{ filePath }; + for (const auto & line : lines) + { + outputFileStream << line << '\n'; + } +} + +template +bool +StringStartsWith(const std::string & str, const char (&substr)[VLength]) +{ + assert(substr[VLength - 1] == '\0'); + return str.compare(0, VLength - 1, substr) == 0; +} + +template +bool +StringEndsWith(const std::string & str, const char (&substr)[VLength]) +{ + assert(substr[VLength - 1] == '\0'); + return str.size() >= VLength && str.compare(str.size() + 1 - VLength, VLength - 1, substr) == 0; +} + +unsigned int +AddInitializers(Lines & lines) +{ + auto numberOfAddedInitializers = 0u; + + bool shouldThisClassHaveInitializers{ false }; + + for (auto & line : lines) + { + const auto lineStartsWith = [&line](const auto & substr) { return StringStartsWith(line, substr); }; + const auto lineEndsWith = [&line](const auto & substr) { return StringEndsWith(line, substr); }; + const auto lineContains = [&line](const auto & substr) { return line.find(substr) != std::string::npos; }; + + const auto numberOfChars = line.size(); + + if (numberOfChars > 0) + { + if (lineStartsWith(" itkNewMacro(") || lineStartsWith(" itkSimpleNewMacro(") || + lineStartsWith(" itkCreateAnotherMacro(") || lineStartsWith(" virtual ") || lineEndsWith(" override") || + lineEndsWith(" override;") || lineEndsWith(" override = default;")) + { + // We assume that a class should *certainly* have in-class data member initializers when it has a call to one + // of those macro's (itkNewMacro, itkSimpleNewMacro, or itkCreateAnotherMacro), or when it has a virtual member + // function. The creation of an object of such a class is already quite expensive (relative to a simple data + // struct), so the extra potential performance penalty of initializing all of its data should be neglectable. + shouldThisClassHaveInitializers = true; + } + else + { + if (shouldThisClassHaveInitializers) + { + if (lineStartsWith("};")) + { + // Class definition ended here. + shouldThisClassHaveInitializers = false; + } + else + { + // Note: Data members of type std::unique_ptr or itk::SmartPointer are excluded from automatically + // adding initializers, because they _might_ get compilation errors from GCC (prior to GCC release 9.2) when + // the template argument T is an incomplete type (a forwarded class) as addressed by Simon Rit: + // - pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3877 + // - commit eac289d25221d8e080a0ad6d2e6ce6ff0a2c576a + // - "COMP: Remove in-class {} member initializers of unique_ptr" + // - pull request https://github.com/InsightSoftwareConsortium/ITK/pull/3927 + // - commit f5f83678755319e35c225bfe563fb26ef1d0197e + // - "COMP: Remove in class init of SmartPointer of forward declaration" + // Data members of a C++ reference type (" & m_") and `static` data members are also excluded. + // `m_ColorTable` was excluded, to avoid GCC warnings saying "const/copy propagation disabled " and " GCSE + // disabled " [-Wdisabled-optimization], at line + // https://github.com/InsightSoftwareConsortium/ITK/blob/v5.3.0/Modules/Core/Common/include/itkOctree.h#L222 + + if (lineStartsWith(" ") && lineContains(" m_") && !lineContains(" & m_") && !lineContains(" static ") && + !lineContains(" std::unique_ptr<") && !lineContains("SmartPointer<") && + !lineContains("m_ColorTable[ColorTableSize];")) + { + // The regular expression explicitly skips lines of code that have a `)` at the end, like + // ` itkSetVectorMacro(NumberOfIterations, unsigned int, m_NumberOfLevels);`, from + // https://github.com/InsightSoftwareConsortium/ITK/blob/v5.3.0/Modules/Registration/PDEDeformable/include/itkMultiResolutionPDEDeformableRegistration.h#L224 + + if (std::regex_match(line, std::regex{ R"delimiter( \w.* m_\w([^{}=]*[^{}=\)])?;)delimiter" })) + { + line.pop_back(); + line.append("{};"); + std::cout << line << '\n'; + ++numberOfAddedInitializers; + } + else + { + // Address lines of code that have a trailing C++ comment. + std::match_results results; + if (std::regex_match( + line, + results, + std::regex{ R"delimiter(( \w.* m_\w(?:[^{}=]*[^{}=\)])?);([ ]+/[/\*].+))delimiter" }) && + results.size() == 3) + { + line = results[1].str() + "{};" + results[2].str(); + std::cout << line << '\n'; + ++numberOfAddedInitializers; + } + } + } + } + } + } + } + } + return numberOfAddedInitializers; +} + +auto +ProcessFile(const path & filePath) +{ + auto lines = ReadFile(filePath); + const auto numberOfAddedInitializers = AddInitializers(lines); + if (numberOfAddedInitializers > 0) + { + std::cout << "Added " << numberOfAddedInitializers << " initializer(s) to " << filePath.string() << std::endl; + WriteFile(filePath, lines); + } + return numberOfAddedInitializers; +} + +void +ProcessDirectory(const path & directoryPath) +{ + auto numberOfAddedInitializers = 0u; + + for (const auto & entry : recursive_directory_iterator{ directoryPath }) + { + const auto & path = entry.path(); + const auto & extension = path.extension(); + + if ((!extension.empty()) && extension.string() == ".h" && StringStartsWith(path.stem().string(), "itk") && + is_regular_file(path)) + { + numberOfAddedInitializers += ProcessFile(path); + } + } + std::cout << "Added in total " << numberOfAddedInitializers << " initializer(s) to " << directoryPath.string() + << std::endl; +} +} // namespace + + +int +main(int argc, char ** argv) +{ + if (argc != 2) + { + std::cout << "Please specify the source directory path as command-line argument." + "\nNote: This program will modify the source files in-place!!!" + << std::endl; + } + else + { + if (argv == nullptr) + { + return EXIT_FAILURE; + } + const char * const arg = argv[1]; + + if (arg == nullptr) + { + return EXIT_FAILURE; + } + ProcessDirectory(arg); + } + + std::cout << "Press anything to continue" << std::endl; + std::cin.get(); + return EXIT_SUCCESS; +}