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)