aboutsummaryrefslogtreecommitdiffstats
path: root/dev-scripts/generate.py
diff options
context:
space:
mode:
Diffstat (limited to 'dev-scripts/generate.py')
-rwxr-xr-xdev-scripts/generate.py548
1 files changed, 548 insertions, 0 deletions
diff --git a/dev-scripts/generate.py b/dev-scripts/generate.py
new file mode 100755
index 00000000..1b7633ed
--- /dev/null
+++ b/dev-scripts/generate.py
@@ -0,0 +1,548 @@
+#!/usr/bin/env python
+
+_license_text = \
+"""/*
+ This file is part of the clazy static checker.
+
+ Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+ Author: SĂ©rgio Martins <sergio.martins@kdab.com>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Library General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Library General Public License for more details.
+
+ You should have received a copy of the GNU Library General Public License
+ along with this library; see the file COPYING.LIB. If not, write to
+ the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA 02110-1301, USA.
+*/
+"""
+
+import sys, os, json, argparse, datetime, io
+from shutil import copyfile
+
+CHECKS_FILENAME = 'checks.json'
+_checks = []
+_specified_check_names = []
+_available_categories = []
+
+def checkSortKey(check):
+ return str(check.level) + check.name
+
+def level_num_to_enum(n):
+ if n == -1:
+ return 'ManualCheckLevel'
+ if n >= 0 and n <= 3:
+ return 'CheckLevel' + str(n)
+
+ return 'CheckLevelUndefined'
+
+def level_num_to_name(n):
+ if n == -1:
+ return 'Manual Level'
+ if n >= 0 and n <= 3:
+ return 'Level ' + str(n)
+
+ return 'undefined'
+
+def level_num_to_cmake_readme_variable(n):
+ if n == -1:
+ return 'README_manuallevel_FILES'
+ if n >= 0 and n <= 3:
+ return 'README_LEVEL%s_FILES' % str(n)
+
+ return 'undefined'
+
+def clazy_source_path():
+ return os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/..") + "/"
+
+def templates_path():
+ return clazy_source_path() + "dev-scripts/templates/"
+
+def docs_relative_path():
+ return "docs/checks/"
+
+def docs_path():
+ return clazy_source_path() + docs_relative_path()
+
+def read_file(filename):
+ f = io.open(filename, 'r', newline='\n', encoding='utf8')
+ contents = f.read()
+ f.close()
+ return contents
+
+def write_file(filename, contents):
+ f = io.open(filename, 'w', newline='\n', encoding='utf8')
+ f.write(contents)
+ f.close()
+
+def get_copyright():
+ year = datetime.datetime.now().year
+ author = os.getenv('GIT_AUTHOR_NAME', 'Author')
+ email = os.getenv('GIT_AUTHOR_EMAIL', 'your@email')
+ return "Copyright (C) %s %s <%s>" % (year, author, email)
+
+class Check:
+ def __init__(self):
+ self.name = ""
+ self.class_name = ""
+ self.level = 0
+ self.categories = []
+ self.minimum_qt_version = 40000 # Qt 4.0.0
+ self.fixits = []
+ self.visits_stmts = False
+ self.visits_decls = False
+ self.ifndef = ""
+
+ def include(self): # Returns for example: "returning-void-expression.h"
+ oldstyle_headername = (self.name + ".h").replace('-', '')
+ if os.path.exists(self.path() + oldstyle_headername):
+ return oldstyle_headername
+
+ return self.name + '.h'
+
+ def qualified_include(self): # Returns for example: "checks/level2/returning-void-expression.h"
+ return self.basedir() + self.include()
+
+ def qualified_cpp_filename(self): # Returns for example: "checks/level2/returning-void-expression.cpp"
+ return self.basedir() + self.cpp_filename()
+
+ def cpp_filename(self): # Returns for example: "returning-void-expression.cpp"
+ filename = self.include()
+ filename = filename.replace(".h", ".cpp")
+ return filename
+
+ def path(self):
+ return clazy_source_path() + self.basedir(True) + "/"
+
+ def basedir(self, with_src=False):
+ level = 'level' + str(self.level)
+ if self.level == -1:
+ level = 'manuallevel'
+
+ if with_src:
+ return "src/checks/" + level + '/'
+ return "checks/" + level + '/'
+
+ def readme_name(self):
+ return "README-" + self.name + ".md"
+
+ def readme_path(self):
+ return docs_path() + self.readme_name()
+
+
+ def supportsQt4(self):
+ return self.minimum_qt_version < 50000
+
+ def get_class_name(self):
+ if self.class_name:
+ return self.class_name
+
+ # Deduce the class name
+ splitted = self.name.split('-')
+ classname = ""
+ for word in splitted:
+ if word == 'qt':
+ word = 'Qt'
+ else:
+ word = word.title()
+ if word.startswith('Q'):
+ word = 'Q' + word[1:].title()
+
+ classname += word
+
+ return classname
+
+ def valid_name(self):
+ if self.name in ['clazy']:
+ return False
+ if self.name.startswith('level'):
+ return False
+ if self.name.startswith('fix'):
+ return False
+ return True
+
+ def fixits_text(self):
+ if not self.fixits:
+ return ""
+
+ text = ""
+ fixitnames = []
+ for f in self.fixits:
+ fixitnames.append("fix-" + f)
+
+ text = ','.join(fixitnames)
+
+ return "(" + text + ")"
+
+ def include_guard(self):
+ guard = self.name.replace('-', '_')
+ return guard.upper()
+
+
+def load_json(filename):
+ jsonContents = read_file(filename)
+ decodedJson = json.loads(jsonContents)
+
+ if 'checks' not in decodedJson:
+ print("No checks found in " + filename)
+ return False
+
+ checks = decodedJson['checks']
+
+ global _available_categories, _checks, _specified_check_names
+
+ if 'available_categories' in decodedJson:
+ _available_categories = decodedJson['available_categories']
+
+ for check in checks:
+ c = Check()
+ try:
+ c.name = check['name']
+ c.level = check['level']
+ if 'categories' in check:
+ c.categories = check['categories']
+ for cat in c.categories:
+ if cat not in _available_categories:
+ print('Unknown category ' + cat)
+ return False
+ except KeyError:
+ print("Missing mandatory field while processing " + str(check))
+ return False
+
+ if _specified_check_names and c.name not in _specified_check_names:
+ continue
+
+ if 'class_name' in check:
+ c.class_name = check['class_name']
+
+ if 'ifndef' in check:
+ c.ifndef = check['ifndef']
+
+ if 'minimum_qt_version' in check:
+ c.minimum_qt_version = check['minimum_qt_version']
+
+ if 'visits_stmts' in check:
+ c.visits_stmts = check['visits_stmts']
+
+ if 'visits_decls' in check:
+ c.visits_decls = check['visits_decls']
+
+ if 'fixits' in check:
+ for fixit in check['fixits']:
+ if 'name' not in fixit:
+ print('fixit doesnt have a name. check=' + str(check))
+ return False
+ c.fixits.append(fixit['name'])
+
+ if not c.valid_name():
+ print("Invalid check name: %s" % (c.name()))
+ return False
+ _checks.append(c)
+
+ _checks = sorted(_checks, key=checkSortKey)
+ return True
+
+def print_checks(checks):
+ for c in checks:
+ print(c.name + " " + str(c.level) + " " + str(c.categories))
+
+#-------------------------------------------------------------------------------
+def generate_register_checks(checks):
+ text = '#include "checkmanager.h"\n'
+ for c in checks:
+ text += '#include "' + c.qualified_include() + '"\n'
+ text += \
+"""
+template <typename T>
+RegisteredCheck check(const char *name, CheckLevel level, RegisteredCheck::Options options = RegisteredCheck::Option_None)
+{
+ auto factoryFuntion = [name](ClazyContext *context){ return new T(name, context); };
+ return RegisteredCheck{name, level, factoryFuntion, options};
+}
+
+void CheckManager::registerChecks()
+{
+"""
+
+ for c in checks:
+ qt4flag = "RegisteredCheck::Option_None"
+ if not c.supportsQt4():
+ qt4flag = "RegisteredCheck::Option_Qt4Incompatible"
+
+ if c.visits_stmts:
+ qt4flag += " | RegisteredCheck::Option_VisitsStmts"
+ if c.visits_decls:
+ qt4flag += " | RegisteredCheck::Option_VisitsDecls"
+
+ qt4flag = qt4flag.replace("RegisteredCheck::Option_None |", "")
+
+ if c.ifndef:
+ text += "#ifndef " + c.ifndef + "\n"
+
+ text += ' registerCheck(check<%s>("%s", %s, %s));\n' % (c.get_class_name(), c.name, level_num_to_enum(c.level), qt4flag)
+
+ fixitID = 1
+ for fixit in c.fixits:
+ text += ' registerFixIt(%d, "%s", "%s");\n' % (fixitID, "fix-" + fixit, c.name)
+ fixitID = fixitID * 2
+
+ if c.ifndef:
+ text += "#endif" + "\n"
+
+ text += "}\n"
+
+ comment_text = \
+"""
+/**
+ * To add a new check you can either edit this file, or use the python script:
+ * dev-scripts/generate.py > src/Checks.h
+ */
+"""
+ text = _license_text + '\n' + comment_text + '\n' + text
+ filename = clazy_source_path() + "src/Checks.h"
+
+ old_text = read_file(filename)
+ if old_text != text:
+ write_file(filename, text)
+ print("Generated " + filename)
+ return True
+ return False
+#-------------------------------------------------------------------------------
+def generate_cmake_file(checks):
+ text = "set(CLAZY_CHECKS_SRCS ${CLAZY_CHECKS_SRCS}\n"
+ checks_with_regexp = []
+ for level in [-1, 0, 1, 2, 3]:
+ for check in checks:
+ if check.level == level:
+ text += " ${CMAKE_CURRENT_LIST_DIR}/src/" + check.qualified_cpp_filename() + "\n"
+ if check.ifndef == "NO_STD_REGEX":
+ checks_with_regexp.append(check)
+ text += ")\n"
+
+ if checks_with_regexp:
+ text += "\nif(HAS_STD_REGEX OR CLAZY_BUILD_WITH_CLANG)\n"
+ for check in checks_with_regexp:
+ text += " set(CLAZY_CHECKS_SRCS ${CLAZY_CHECKS_SRCS} ${CMAKE_CURRENT_LIST_DIR}/src/" + check.qualified_cpp_filename() + ")\n"
+ text += "endif()\n"
+
+ filename = clazy_source_path() + "CheckSources.cmake"
+ old_text = read_file(filename)
+ if old_text != text:
+ write_file(filename, text)
+ print("Generated " + filename)
+ return True
+ return False
+#-------------------------------------------------------------------------------
+def create_readmes(checks):
+ generated = False
+ for check in checks:
+ if not os.path.exists(check.readme_path()):
+ existing_readme = search_in_all_levels(check.readme_name())
+ if existing_readme:
+ contents = read_file(existing_readme)
+ write_file(check.readme_path(), contents)
+ os.remove(existing_readme)
+ print("Moved " + check.readme_name())
+ else:
+ contents = read_file(templates_path() + "check-readme.md")
+ contents = contents.replace('[check-name]', check.name)
+ write_file(check.readme_path(), contents)
+ print("Created " + check.readme_path())
+ generated = True
+ return generated
+#-------------------------------------------------------------------------------
+def create_unittests(checks):
+ generated = False
+ for check in checks:
+ unittest_folder = clazy_source_path() + "tests/" + check.name
+ if not os.path.exists(unittest_folder):
+ os.mkdir(unittest_folder)
+ print("Created " + unittest_folder)
+ generated = True
+
+ configjson_file = unittest_folder + "/config.json"
+ if not os.path.exists(configjson_file):
+ copyfile(templates_path() + "test-config.json", configjson_file)
+ print("Created " + configjson_file)
+ generated = True
+
+ testmain_file = unittest_folder + "/main.cpp"
+ if not os.path.exists(testmain_file) and check.name != 'non-pod-global-static':
+ copyfile(templates_path() + "test-main.cpp", testmain_file)
+ print("Created " + testmain_file)
+ generated = True
+ return generated
+
+#-------------------------------------------------------------------------------
+def search_in_all_levels(filename):
+ for level in ['manuallevel', 'level0', 'level1', 'level2', 'level3']:
+ complete_filename = clazy_source_path() + 'src/checks/' + level + '/' + filename
+ if os.path.exists(complete_filename):
+ return complete_filename
+ return ""
+
+#-------------------------------------------------------------------------------
+def create_checks(checks):
+ generated = False
+ edit_changelog = False
+ for check in checks:
+ include_file = check.path() + check.include()
+ cpp_file = check.path() + check.cpp_filename()
+ copyright = get_copyright()
+ include_missing = not os.path.exists(include_file)
+ cpp_missing = not os.path.exists(cpp_file)
+ if include_missing:
+
+ existing_include_path = search_in_all_levels(check.include())
+ if existing_include_path:
+ # File already exists, but is in another level. Just move it:
+ contents = read_file(existing_include_path)
+ write_file(include_file, contents)
+ os.remove(existing_include_path)
+ print("Moved " + check.include())
+ else:
+ contents = read_file(templates_path() + 'check.h')
+ contents = contents.replace('%1', check.include_guard())
+ contents = contents.replace('%2', check.get_class_name())
+ contents = contents.replace('%3', check.name)
+ contents = contents.replace('%4', copyright)
+ write_file(include_file, contents)
+ print("Created " + include_file)
+ edit_changelog = True
+ generated = True
+ if cpp_missing:
+ existing_cpp_path = search_in_all_levels(check.cpp_filename())
+ if existing_cpp_path:
+ # File already exists, but is in another level. Just move it:
+ contents = read_file(existing_cpp_path)
+ write_file(cpp_file, contents)
+ os.remove(existing_cpp_path)
+ print("Moved " + check.cpp_filename())
+ else:
+ contents = read_file(templates_path() + 'check.cpp')
+ contents = contents.replace('%1', check.include())
+ contents = contents.replace('%2', check.get_class_name())
+ contents = contents.replace('%3', copyright)
+ write_file(cpp_file, contents)
+ print("Created " + cpp_file)
+ generated = True
+
+ if edit_changelog:
+ # We created a new check, let's also edit the ChangeLog
+ changelog_file = clazy_source_path() + 'Changelog'
+ contents = read_file(changelog_file)
+ contents += '\n - <dont forget changelog entry for ' + check.name + '>\n'
+ write_file(changelog_file, contents)
+ print('Edited Changelog')
+
+ return generated
+#-------------------------------------------------------------------------------
+def generate_readme(checks):
+ filename = clazy_source_path() + "README.md"
+ f = io.open(filename, 'r', newline='\n', encoding='utf8')
+ old_contents = f.readlines();
+ f.close();
+
+ new_text_to_insert = ""
+ for level in ['-1', '0', '1', '2', '3']:
+ new_text_to_insert += "- Checks from %s:" % level_num_to_name(int(level)) + "\n"
+ for c in checks:
+ if str(c.level) == level:
+ fixits_text = c.fixits_text()
+ if fixits_text:
+ fixits_text = " " + fixits_text
+ new_text_to_insert += " - [%s](%sREADME-%s.md)%s" % (c.name, docs_relative_path(), c.name, fixits_text) + "\n"
+ new_text_to_insert += "\n"
+
+
+ f = io.open(filename, 'w', newline='\n', encoding='utf8')
+
+ skip = False
+ for line in old_contents:
+ if skip and line.startswith("#"):
+ skip = False
+
+ if skip:
+ continue
+
+ if line.startswith("- Checks from Manual Level:"):
+ skip = True
+ f.write(new_text_to_insert)
+ continue
+
+ f.write(line)
+ f.close()
+
+ f = io.open(filename, 'r', newline='\n', encoding='utf8')
+ new_contents = f.readlines();
+ f.close();
+
+ if old_contents != new_contents:
+ print("Generated " + filename)
+ return True
+ return False
+#-------------------------------------------------------------------------------
+def generate_readmes_cmake_install(checks):
+ old_contents = ""
+ filename = clazy_source_path() + 'readmes.cmake'
+ if os.path.exists(filename):
+ f = io.open(filename, 'r', newline='\n', encoding='utf8')
+ old_contents = f.readlines();
+ f.close();
+
+ new_text_to_insert = ""
+ for level in ['-1', '0', '1', '2', '3']:
+ new_text_to_insert += 'SET(' + level_num_to_cmake_readme_variable(int(level)) + "\n"
+ for c in checks:
+ if str(c.level) == level:
+ new_text_to_insert += ' ${CMAKE_CURRENT_LIST_DIR}/docs/checks/' + c.readme_name() + '\n'
+ new_text_to_insert += ')\n\n'
+
+ if old_contents == new_text_to_insert:
+ return False
+
+ f = io.open(filename, 'w', newline='\n', encoding='utf8')
+ f.write(new_text_to_insert)
+ f.close()
+ return True
+
+#-------------------------------------------------------------------------------
+
+complete_json_filename = clazy_source_path() + CHECKS_FILENAME
+
+if not os.path.exists(complete_json_filename):
+ print("File doesn't exist: " + complete_json_filename)
+ exit(1)
+
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--generate", action='store_true', help="Generate src/Checks.h, CheckSources.cmake and README.md")
+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.")
+args = parser.parse_args()
+
+_specified_check_names = args.checks
+
+if not load_json(complete_json_filename):
+ exit(1)
+
+if args.generate:
+ generated = False
+ generated = generate_register_checks(_checks) or generated
+ generated = generate_cmake_file(_checks) or generated
+ generated = generate_readme(_checks) or generated
+ generated = create_readmes(_checks) or generated
+ generated = create_unittests(_checks) or generated
+ generated = create_checks(_checks) or generated
+ generated = generate_readmes_cmake_install(_checks) or generated
+ if not generated:
+ print("Nothing to do, everything is OK")
+else:
+ parser.print_help(sys.stderr)