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