File indexing completed on 2024-04-21 05:38:35

0001 #!/usr/bin/env python3
0002 
0003 _license_text = \
0004 """/*
0005     SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company info@kdab.com
0006     SPDX-FileContributor: SĂ©rgio Martins <sergio.martins@kdab.com>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 """
0011 
0012 import sys, os, json, argparse, datetime, io, subprocess
0013 from shutil import copyfile
0014 
0015 CHECKS_FILENAME = 'checks.json'
0016 _checks = []
0017 _specified_check_names = []
0018 _available_categories = []
0019 
0020 def checkSortKey(check):
0021     return str(check.level) + check.name
0022 
0023 def level_num_to_enum(n):
0024     if n == -1:
0025         return 'ManualCheckLevel'
0026     if n >= 0 and n <= 3:
0027         return 'CheckLevel' + str(n)
0028 
0029     return 'CheckLevelUndefined'
0030 
0031 def level_num_to_name(n):
0032     if n == -1:
0033         return 'Manual Level'
0034     if n >= 0 and n <= 3:
0035         return 'Level ' + str(n)
0036 
0037     return 'undefined'
0038 
0039 def level_num_to_cmake_readme_variable(n):
0040     if n == -1:
0041         return 'README_manuallevel_FILES'
0042     if n >= 0 and n <= 3:
0043         return 'README_LEVEL%s_FILES' % str(n)
0044 
0045     return 'undefined'
0046 
0047 def clazy_source_path():
0048     return os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/..") + "/"
0049 
0050 def templates_path():
0051     return clazy_source_path() + "dev-scripts/templates/"
0052 
0053 def docs_relative_path():
0054     return "docs/checks/"
0055 
0056 def docs_path():
0057     return clazy_source_path() + docs_relative_path()
0058 
0059 def read_file(filename):
0060     f = io.open(filename, 'r', newline='\n', encoding='utf8')
0061     contents = f.read()
0062     f.close()
0063     return contents
0064 
0065 def write_file(filename: str, contents):
0066     f = io.open(filename, 'w', newline='\n', encoding='utf8')
0067     f.write(contents)
0068     f.close()
0069     if filename.endswith('.h') or filename.endswith('.cpp'):
0070         subprocess.run(['clang-format', '-i', filename])
0071 
0072 def get_copyright():
0073     year = datetime.datetime.now().year
0074     author = os.getenv('GIT_AUTHOR_NAME', 'Author')
0075     email = os.getenv('GIT_AUTHOR_EMAIL', 'your@email')
0076     return "Copyright (C) %s %s <%s>" % (year, author, email)
0077 
0078 class Check:
0079     def __init__(self):
0080         self.name = ""
0081         self.class_name = ""
0082         self.level = 0
0083         self.categories = []
0084         self.minimum_qt_version = 40000 # Qt 4.0.0
0085         self.fixits = []
0086         self.visits_stmts = False
0087         self.visits_decls = False
0088         self.ifndef = ""
0089 
0090     def include(self): # Returns for example: "returning-void-expression.h"
0091         oldstyle_headername = (self.name + ".h").replace('-', '')
0092         if os.path.exists(self.path() + oldstyle_headername):
0093             return oldstyle_headername
0094 
0095         return self.name + '.h'
0096 
0097     def qualified_include(self): # Returns for example: "checks/level2/returning-void-expression.h"
0098         return self.basedir() + self.include()
0099 
0100     def qualified_cpp_filename(self): # Returns for example: "checks/level2/returning-void-expression.cpp"
0101         return self.basedir() + self.cpp_filename()
0102 
0103     def cpp_filename(self): # Returns for example: "returning-void-expression.cpp"
0104         filename = self.include()
0105         filename = filename.replace(".h", ".cpp")
0106         return filename
0107 
0108     def path(self):
0109         return clazy_source_path() + self.basedir(True) + "/"
0110 
0111     def basedir(self, with_src=False):
0112         level = 'level' + str(self.level)
0113         if self.level == -1:
0114             level = 'manuallevel'
0115 
0116         if with_src:
0117             return "src/checks/" + level + '/'
0118         return "checks/" + level + '/'
0119 
0120     def readme_name(self):
0121         return "README-" + self.name + ".md"
0122 
0123     def readme_path(self):
0124         return docs_path() + self.readme_name()
0125 
0126 
0127     def supportsQt4(self):
0128         return self.minimum_qt_version < 50000
0129 
0130     def get_class_name(self):
0131         if self.class_name:
0132             return self.class_name
0133 
0134         # Deduce the class name
0135         splitted = self.name.split('-')
0136         classname = ""
0137         for word in splitted:
0138             if word == 'qt':
0139                 word = 'Qt'
0140             else:
0141                 word = word.title()
0142                 if word.startswith('Q'):
0143                     word = 'Q' + word[1:].title()
0144 
0145             classname += word
0146 
0147         return classname
0148 
0149     def valid_name(self):
0150         if self.name in ['clazy']:
0151             return False
0152         if self.name.startswith('level'):
0153             return False
0154         if self.name.startswith('fix'):
0155             return False
0156         return True
0157 
0158     def fixits_text(self):
0159         if not self.fixits:
0160             return ""
0161 
0162         text = ""
0163         fixitnames = []
0164         for f in self.fixits:
0165             fixitnames.append("fix-" + f)
0166 
0167         text = ','.join(fixitnames)
0168 
0169         return "(" + text + ")"
0170 
0171     def include_guard(self):
0172         guard = self.name.replace('-', '_')
0173         return guard.upper()
0174 
0175 
0176 def load_json(filename):
0177     jsonContents = read_file(filename)
0178     decodedJson = json.loads(jsonContents)
0179 
0180     if 'checks' not in decodedJson:
0181         print("No checks found in " + filename)
0182         return False
0183 
0184     checks = decodedJson['checks']
0185 
0186     global _available_categories, _checks, _specified_check_names
0187 
0188     if 'available_categories' in decodedJson:
0189         _available_categories = decodedJson['available_categories']
0190 
0191     for check in checks:
0192         c = Check()
0193         try:
0194             c.name = check['name']
0195             c.level = check['level']
0196             if 'categories' in check:
0197                 c.categories = check['categories']
0198             for cat in c.categories:
0199                 if cat not in _available_categories:
0200                     print('Unknown category ' + cat)
0201                     return False
0202         except KeyError:
0203             print("Missing mandatory field while processing " + str(check))
0204             return False
0205 
0206         if _specified_check_names and c.name not in _specified_check_names:
0207             continue
0208 
0209         if 'class_name' in check:
0210             c.class_name = check['class_name']
0211 
0212         if 'ifndef' in check:
0213             c.ifndef = check['ifndef']
0214 
0215         if 'minimum_qt_version' in check:
0216             c.minimum_qt_version = check['minimum_qt_version']
0217 
0218         if 'visits_stmts' in check:
0219             c.visits_stmts = check['visits_stmts']
0220 
0221         if 'visits_decls' in check:
0222             c.visits_decls = check['visits_decls']
0223 
0224         if 'fixits' in check:
0225             for fixit in check['fixits']:
0226                 if 'name' not in fixit:
0227                     print('fixit doesnt have a name. check=' + str(check))
0228                     return False
0229                 c.fixits.append(fixit['name'])
0230 
0231         if not c.valid_name():
0232             print("Invalid check name: %s" % (c.name()))
0233             return False
0234         _checks.append(c)
0235 
0236     _checks = sorted(_checks, key=checkSortKey)
0237     return True
0238 
0239 def print_checks(checks):
0240     for c in checks:
0241         print(c.name + " " + str(c.level) + " " + str(c.categories))
0242 
0243 #-------------------------------------------------------------------------------
0244 def generate_register_checks(checks):
0245     text = '#include "checkmanager.h"\n'
0246     for c in checks:
0247         text += '#include "' + c.qualified_include() + '"\n'
0248     text += \
0249 """
0250 template <typename T>
0251 RegisteredCheck check(const char *name, CheckLevel level, RegisteredCheck::Options options = RegisteredCheck::Option_None)
0252 {
0253     auto factoryFuntion = [name](ClazyContext *context){ return new T(name, context); };
0254     return RegisteredCheck{name, level, factoryFuntion, options};
0255 }
0256 
0257 void CheckManager::registerChecks()
0258 {
0259 """
0260 
0261     for c in checks:
0262         qt4flag = "RegisteredCheck::Option_None"
0263         if not c.supportsQt4():
0264             qt4flag = "RegisteredCheck::Option_Qt4Incompatible"
0265 
0266         if c.visits_stmts:
0267             qt4flag += " | RegisteredCheck::Option_VisitsStmts"
0268         if c.visits_decls:
0269             qt4flag += " | RegisteredCheck::Option_VisitsDecls"
0270 
0271         qt4flag = qt4flag.replace("RegisteredCheck::Option_None |", "")
0272 
0273         if c.ifndef:
0274             text += "#ifndef " + c.ifndef + "\n"
0275 
0276         text += '    registerCheck(check<%s>("%s", %s, %s));\n' % (c.get_class_name(), c.name, level_num_to_enum(c.level), qt4flag)
0277 
0278         fixitID = 1
0279         for fixit in c.fixits:
0280             text += '    registerFixIt(%d, "%s", "%s");\n' % (fixitID, "fix-" + fixit, c.name)
0281             fixitID = fixitID * 2
0282 
0283         if c.ifndef:
0284             text += "#endif" + "\n"
0285 
0286     text += "}\n"
0287 
0288     comment_text = \
0289 """
0290 /**
0291  * New scripts should be added to the check.json file and the files should be regenerated
0292  * ./dev-scripts/generate.py --generate
0293  */
0294 """
0295     text = _license_text + '\n' + comment_text + '\n' + text
0296     filename = clazy_source_path() + "src/Checks.h"
0297 
0298     old_text = read_file(filename)
0299     if old_text != text:
0300         write_file(filename, text)
0301         print("Generated " + filename)
0302         return True
0303     return False
0304 #-------------------------------------------------------------------------------
0305 def generate_cmake_file(checks):
0306     text = "# This file was autogenerated by running: ./dev-scripts/generate.py --generate\n\nset(CLAZY_CHECKS_SRCS ${CLAZY_CHECKS_SRCS}\n"
0307     for level in [-1, 0, 1, 2, 3]:
0308         for check in checks:
0309             if check.level == level:
0310                 text += "  ${CMAKE_CURRENT_LIST_DIR}/src/" + check.qualified_cpp_filename() + "\n"
0311     text += ")\n"
0312 
0313     filename = clazy_source_path() + "CheckSources.generated.cmake"
0314     old_text = read_file(filename)
0315     if old_text != text:
0316         write_file(filename, text)
0317         print("Generated " + filename)
0318         return True
0319     return False
0320 #-------------------------------------------------------------------------------
0321 def create_readmes(checks):
0322     generated = False
0323     for check in checks:
0324         if not os.path.exists(check.readme_path()):
0325             existing_readme = search_in_all_levels(check.readme_name())
0326             if existing_readme:
0327                 contents = read_file(existing_readme)
0328                 write_file(check.readme_path(), contents)
0329                 os.remove(existing_readme)
0330                 print("Moved " + check.readme_name())
0331             else:
0332                 contents = read_file(templates_path() + "check-readme.md")
0333                 contents = contents.replace('[check-name]', check.name)
0334                 write_file(check.readme_path(), contents)
0335                 print("Created " + check.readme_path())
0336             generated = True
0337     return generated
0338 #-------------------------------------------------------------------------------
0339 def create_unittests(checks):
0340     generated = False
0341     for check in checks:
0342         unittest_folder = clazy_source_path() + "tests/" + check.name
0343         if not os.path.exists(unittest_folder):
0344             os.mkdir(unittest_folder)
0345             print("Created " + unittest_folder)
0346             generated = True
0347 
0348         configjson_file = unittest_folder + "/config.json"
0349         if not os.path.exists(configjson_file):
0350             copyfile(templates_path() + "test-config.json", configjson_file)
0351             print("Created " + configjson_file)
0352             generated = True
0353 
0354         testmain_file = unittest_folder + "/main.cpp"
0355         if not os.path.exists(testmain_file) and check.name != 'non-pod-global-static':
0356             copyfile(templates_path() + "test-main.cpp", testmain_file)
0357             print("Created " + testmain_file)
0358             generated = True
0359     return generated
0360 
0361 #-------------------------------------------------------------------------------
0362 def search_in_all_levels(filename):
0363     for level in ['manuallevel', 'level0', 'level1', 'level2']:
0364         complete_filename = clazy_source_path() + 'src/checks/' + level + '/' + filename
0365         if os.path.exists(complete_filename):
0366             return complete_filename
0367     return ""
0368 
0369 #-------------------------------------------------------------------------------
0370 def create_checks(checks):
0371     generated = False
0372 
0373     for check in checks:
0374         edit_changelog = False
0375         include_file = check.path() + check.include()
0376         cpp_file = check.path() + check.cpp_filename()
0377         copyright = get_copyright()
0378         include_missing = not os.path.exists(include_file)
0379         cpp_missing = not os.path.exists(cpp_file)
0380         if include_missing:
0381 
0382             existing_include_path = search_in_all_levels(check.include())
0383             if existing_include_path:
0384                 # File already exists, but is in another level. Just move it:
0385                 contents = read_file(existing_include_path)
0386                 write_file(include_file, contents)
0387                 os.remove(existing_include_path)
0388                 print("Moved " + check.include())
0389             else:
0390                 contents = read_file(templates_path() + 'check.h')
0391                 contents = contents.replace('%1', check.include_guard())
0392                 contents = contents.replace('%2', check.get_class_name())
0393                 contents = contents.replace('%3', check.name)
0394                 contents = contents.replace('%4', copyright)
0395                 write_file(include_file, contents)
0396                 print("Created " + include_file)
0397                 edit_changelog = True
0398             generated = True
0399         if cpp_missing:
0400             existing_cpp_path = search_in_all_levels(check.cpp_filename())
0401             if existing_cpp_path:
0402                 # File already exists, but is in another level. Just move it:
0403                 contents = read_file(existing_cpp_path)
0404                 write_file(cpp_file, contents)
0405                 os.remove(existing_cpp_path)
0406                 print("Moved " + check.cpp_filename())
0407             else:
0408                 contents = read_file(templates_path() + 'check.cpp')
0409                 contents = contents.replace('%1', check.include())
0410                 contents = contents.replace('%2', check.get_class_name())
0411                 contents = contents.replace('%3', copyright)
0412                 write_file(cpp_file, contents)
0413                 print("Created " + cpp_file)
0414             generated = True
0415 
0416         if edit_changelog:
0417             # We created a new check, let's also edit the ChangeLog
0418             changelog_file = clazy_source_path() + 'Changelog'
0419             contents = read_file(changelog_file)
0420             contents += '\n  - <dont forget changelog entry for ' + check.name + '>\n'
0421             write_file(changelog_file, contents)
0422             print('Edited Changelog')
0423 
0424     return generated
0425 #-------------------------------------------------------------------------------
0426 def generate_readme(checks):
0427     filename = clazy_source_path() + "README.md"
0428     f = io.open(filename, 'r', newline='\n', encoding='utf8')
0429     old_contents = f.readlines();
0430     f.close();
0431 
0432     new_text_to_insert = ""
0433     for level in ['-1', '0', '1', '2']:
0434         new_text_to_insert += "- Checks from %s:" % level_num_to_name(int(level)) + "\n"
0435         for c in checks:
0436             if str(c.level) == level:
0437                 fixits_text = c.fixits_text()
0438                 if fixits_text:
0439                     fixits_text = "    " + fixits_text
0440                 new_text_to_insert += "    - [%s](%sREADME-%s.md)%s" % (c.name, docs_relative_path(), c.name, fixits_text) + "\n"
0441         new_text_to_insert += "\n"
0442 
0443 
0444     f = io.open(filename, 'w', newline='\n', encoding='utf8')
0445 
0446     skip = False
0447     for line in old_contents:
0448         if skip and line.startswith("#"):
0449             skip = False
0450 
0451         if skip:
0452             continue
0453 
0454         if line.startswith("- Checks from Manual Level:"):
0455             skip = True
0456             f.write(new_text_to_insert)
0457             continue
0458 
0459         f.write(line)
0460     f.close()
0461 
0462     f = io.open(filename, 'r', newline='\n', encoding='utf8')
0463     new_contents = f.readlines();
0464     f.close();
0465 
0466     if old_contents != new_contents:
0467         print("Generated " + filename)
0468         return True
0469     return False
0470 #-------------------------------------------------------------------------------
0471 def generate_ctest(checks):
0472     # Generates the ClazyTests.cmake file
0473     filename = clazy_source_path() + 'ClazyTests.generated.cmake'
0474 
0475     contents = """# This file was autogenerated by running: ./dev-scripts/generate.py --generate\n
0476 macro(add_clazy_test name)
0477   add_test(NAME ${name} COMMAND python3 run_tests.py ${name} --verbose WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/)
0478   set_property(TEST ${name} PROPERTY
0479     ENVIRONMENT "CLAZYPLUGIN_CXX=$<TARGET_FILE:ClazyPlugin>;CLAZYSTANDALONE_CXX=$<TARGET_FILE:clazy-standalone>;$<$<BOOL:${HAS_STD_FILESYSTEM}>:CLAZY_HAS_FILESYSTEM=>"
0480   )
0481 endmacro()\n
0482 """
0483     for c in checks:
0484         contents += 'add_clazy_test(%s)\n' % (c.name)
0485 
0486 
0487     contents += 'add_clazy_test(clazy-standalone)\n'
0488     contents += 'add_clazy_test(clazy)\n'
0489 
0490     f = io.open(filename, 'w', newline='\n', encoding='utf8')
0491     f.write(contents)
0492     f.close()
0493     return True
0494 #-------------------------------------------------------------------------------
0495 def generate_readmes_cmake_install(checks):
0496     old_contents = ""
0497     filename = clazy_source_path() + 'readmes.cmake'
0498     if os.path.exists(filename):
0499         f = io.open(filename, 'r', newline='\n', encoding='utf8')
0500         old_contents = f.readlines();
0501         f.close();
0502 
0503     new_text_to_insert = ""
0504     for level in ['-1', '0', '1', '2']:
0505         new_text_to_insert += 'SET(' + level_num_to_cmake_readme_variable(int(level)) + "\n"
0506         for c in checks:
0507             if str(c.level) == level:
0508                 new_text_to_insert += '    ${CMAKE_CURRENT_LIST_DIR}/docs/checks/' + c.readme_name() + '\n'
0509         new_text_to_insert += ')\n\n'
0510 
0511         if old_contents == new_text_to_insert:
0512             return False
0513 
0514     f = io.open(filename, 'w', newline='\n', encoding='utf8')
0515     f.write(new_text_to_insert)
0516     f.close()
0517     return True
0518 
0519 #-------------------------------------------------------------------------------
0520 
0521 complete_json_filename = clazy_source_path() + CHECKS_FILENAME
0522 
0523 if not os.path.exists(complete_json_filename):
0524     print("File doesn't exist: " + complete_json_filename)
0525     exit(1)
0526 
0527 
0528 
0529 parser = argparse.ArgumentParser()
0530 parser.add_argument("--generate", action='store_true', help="Generate src/Checks.h, CheckSources.cmake and README.md")
0531 parser.add_argument("checks", nargs='*', help="Optional check names to build. Useful to speedup builds during development, by building only the specified checks. Default is to build all checks.")
0532 args = parser.parse_args()
0533 
0534 _specified_check_names = args.checks
0535 
0536 if not load_json(complete_json_filename):
0537     exit(1)
0538 
0539 if args.generate:
0540     generated = False
0541     generated = generate_register_checks(_checks) or generated
0542     generated = generate_cmake_file(_checks) or generated
0543     generated = generate_readme(_checks) or generated
0544     generated = create_readmes(_checks) or generated
0545     generated = create_unittests(_checks) or generated
0546     generated = create_checks(_checks) or generated
0547     generated = generate_readmes_cmake_install(_checks) or generated
0548     generated = generate_ctest(_checks) or generated
0549     if not generated:
0550         print("Nothing to do, everything is OK")
0551 else:
0552     parser.print_help(sys.stderr)