dev-scripts/generate.py (399 lines of code) (raw):
#!/usr/bin/env python3
_license_text = \
"""/*
SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company info@kdab.com
SPDX-FileContributor: Sérgio Martins <sergio.martins@kdab.com>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
"""
import sys, os, json, argparse, datetime, io, subprocess
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: str, contents):
f = io.open(filename, 'w', newline='\n', encoding='utf8')
f.write(contents)
f.close()
if filename.endswith('.h') or filename.endswith('.cpp'):
subprocess.run(['clang-format', '-i', filename])
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 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:
qtflags = "RegisteredCheck::Option_None"
if c.visits_stmts:
qtflags += " | RegisteredCheck::Option_VisitsStmts"
if c.visits_decls:
qtflags += " | RegisteredCheck::Option_VisitsDecls"
qtflags = qtflags.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), qtflags)
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 = \
"""
/**
* New scripts should be added to the check.json file and the files should be regenerated
* ./dev-scripts/generate.py --generate
*/
"""
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 = "# This file was autogenerated by running: ./dev-scripts/generate.py --generate\n\nset(CLAZY_CHECKS_SRCS ${CLAZY_CHECKS_SRCS}\n"
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"
text += ")\n"
filename = clazy_source_path() + "CheckSources.generated.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']:
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
for check in checks:
edit_changelog = False
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']:
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_ctest(checks):
# Generates the ClazyTests.cmake file
filename = clazy_source_path() + 'ClazyTests.generated.cmake'
contents = """# This file was autogenerated by running: ./dev-scripts/generate.py --generate\n
if ($ENV{CI_JOB_NAME_SLUG} MATCHES "qt5")
set(TEST_VERSION_OPTION "--qt-versions=5")
elseif($ENV{CI_JOB_NAME_SLUG} MATCHES "qt6")
set(TEST_VERSION_OPTION "--qt-versions=6")
endif()
macro(add_clazy_test name)
add_test(NAME ${name} COMMAND python3 run_tests.py ${name} --verbose ${TEST_VERSION_OPTION} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests/)
if (CLANG_EXECUTABLE_PATH)
set(CLANG_CXX_TEST "CLANGXX=${CLANG_EXECUTABLE_PATH};")
endif()
set_property(TEST ${name} PROPERTY
ENVIRONMENT "${CLANG_CXX_TEST}CLAZYPLUGIN_CXX=$<TARGET_FILE:ClazyPlugin>;CLAZYSTANDALONE_CXX=$<TARGET_FILE:clazy-standalone>;$<$<BOOL:${HAS_STD_FILESYSTEM}>:CLAZY_HAS_FILESYSTEM=>"
)
endmacro()\n
"""
for c in checks:
contents += 'add_clazy_test(%s)\n' % (c.name)
contents += 'add_clazy_test(clazy-standalone)\n'
contents += 'add_clazy_test(clazy)\n'
f = io.open(filename, 'w', newline='\n', encoding='utf8')
f.write(contents)
f.close()
return True
#-------------------------------------------------------------------------------
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']:
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
generated = generate_ctest(_checks) or generated
if not generated:
print("Nothing to do, everything is OK")
else:
parser.print_help(sys.stderr)