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)