#!/usr/bin/python3 # Copyright (c) 2021 Skyward Experimental Rocketry # Author: Alberto Nidasio # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the 'Software'), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # The linter script offers a lot of functionalities: # - Checks for the right copyright notice in all .h and .cpp files # - Checks if all .h and .cpp files respects the clang format specified in .clang-format file # - Checks if some .h or .cpp files have printfs, asserts or some string used in them import re from collections import Counter from subprocess import DEVNULL, STDOUT, call, run, check_output, CalledProcessError from argparse import ArgumentParser, RawTextHelpFormatter from os import pathconf, walk from os.path import join # Copyright template for C++ code files and headers CPP_TEMPLATE = r'\/\* Copyright \(c\) 20\d\d(?:-20\d\d)? Skyward Experimental Rocketry\n' + \ r' \* (Authors?): (.+)\n' + \ r' \*\n' + \ r' \* Permission is hereby granted, free of charge, to any person obtaining a copy\n' + \ r' \* of this software and associated documentation files \(the "Software"\), to deal\n' + \ r' \* in the Software without restriction, including without limitation the rights\n' + \ r' \* to use, copy, modify, merge, publish, distribute, sublicense, and\/or sell\n' + \ r' \* copies of the Software, and to permit persons to whom the Software is\n' + \ r' \* furnished to do so, subject to the following conditions:\n' + \ r' \*\n' + \ r' \* The above copyright notice and this permission notice shall be included in\n' + \ r' \* all copies or substantial portions of the Software.\n' + \ r' \*\n' + \ r' \* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n' + \ r' \* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n' + \ r' \* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n' + \ r' \* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n' + \ r' \* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n' + \ r' \* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n' + \ r' \* THE SOFTWARE.\n' + \ r' \*\/' AUTHOR_DELIMITER = ',' def config_cmd_parser(): parser = ArgumentParser( description='The linter script offers a lot of functionalities but it does not modify any file. If no option is specified everything will be checked.', formatter_class=RawTextHelpFormatter) parser.add_argument('directory', nargs='?', help='Directory where to search files') parser.add_argument( '--copyright', dest='copyright', action='store_true', help='Checks for the right copyright notice in all .h and .cpp files') parser.add_argument( '--format', dest='format', action='store_true', help='Checks if all .h and .cpp files respects the clang format specified in .clang-format file') parser.add_argument( '--find', dest='find', action='store_true', help='Checks if some .h or .cpp files have printfs, asserts or some string used in them') parser.add_argument( '--cppcheck', dest='cppcheck', action='store_true', help='Runs cppcheck for static code analysis') parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', help='Output only essential messages') return parser def print_banner(): # Font: Ivrit print('+------------------------------+') print('| _ _ _ |') print('| | | (_)_ __ | |_ ___ _ __ |') print('| | | | | \'_ \| __/ _ \ \'__| |') print('| | |___| | | | | || __/ | |') print('| |_____|_|_| |_|\__\___|_| |') print('+------------------------------+') class Colors(): BLACK = '\033[30m' RED = '\033[31m' GREEN = '\033[32m' YELLOW = '\033[33m' BLUE = '\033[34m' MAGENTA = '\033[35m' CYAN = '\033[36m' WHITE = '\033[37m' UNDERLINE = '\033[4m' RESET = '\033[0m' # Checks for the right copyright notice in all .h and .cpp files def check_copyright(directory): print(Colors.GREEN + 'Copyright check' + Colors.RESET) # Statistics totalCheckdFilesCounter = 0 filesWithErrorsCounter = 0 authors = {} averageAuthorsPerFile = 0 # Walk through the directory and check each file for dirpath, dirnames, filenames in walk(directory): for filename in [f for f in filenames if f.endswith(('.cpp', '.h'))]: totalCheckdFilesCounter += 1 # Prepare the complete filepath currentFilepath = join(dirpath, filename) # Check the current file with open(currentFilepath) as file: match = re.search(CPP_TEMPLATE, file.read()) if not match: filesWithErrorsCounter += 1 # The file's copyright notice does not match the template! print(Colors.YELLOW + 'Wrong copyright notice in file {0}'.format( currentFilepath) + Colors.RESET) else: fileAuthors = [a.strip() for a in match.group(2).split(AUTHOR_DELIMITER)] # Check the number of authors against 'Author' or `Authors` if len(fileAuthors) == 1 and match.group(1)[-1] == 's': print('\'Authors\' should to be changed to \'Author\' in {0}'.format( currentFilepath)) if len(fileAuthors) > 1 and match.group(1)[-1] != 's': print('\'Author\' should to be changed to \'Authors\' in {0}'.format( currentFilepath)) # Save statistics on authors for author in fileAuthors: if author in authors: authors[author] += 1 else: authors[author] = 1 averageAuthorsPerFile += len(fileAuthors) averageAuthorsPerFile /= totalCheckdFilesCounter print('Checked {} files'.format(totalCheckdFilesCounter)) if filesWithErrorsCounter == 0: print('All the files have the correct copyright notice') else: print(Colors.RED + '{:.1f}% ({}/{}) of all analyzed files do not match with the copyright template!'.format( 100*filesWithErrorsCounter/totalCheckdFilesCounter, filesWithErrorsCounter, totalCheckdFilesCounter) + Colors.RESET) if (not args.quiet): print('{:.2} authors per file'.format( averageAuthorsPerFile)) print('Number of mentions per author:') for author in sorted(authors.items(), key=lambda item: item[1], reverse=True): print('{:3} - {}'.format(author[1], author[0])) # Exit if error if at least one file isn't correct if (filesWithErrorsCounter > 0): exit(-1) # Checks if all .h and .cpp files respects the clang format specified in .clang-format file def check_format(directory): print(Colors.GREEN + 'Formatting check' + Colors.RESET) # Statistics totalCheckdFilesCounter = 0 filesWithErrorsCounter = 0 # Walk throgh the directory and check each file for dirpath, dirnames, filenames in walk(directory): for filename in [f for f in filenames if f.endswith(('.cpp', '.h', 'c'))]: totalCheckdFilesCounter += 1 # Prepare the complete filepath currentFilepath = join(dirpath, filename) # Dry run clang-format and check if we have an error returnCode = call( ['clang-format', '-style=file', '--dry-run', '--Werror', '--ferror-limit=1', currentFilepath], stderr=DEVNULL) # If and error occurs warn the user if (returnCode != 0): filesWithErrorsCounter += 1 if (not args.quiet): print(Colors.YELLOW + 'Wrong code format for file {1}'.format( returnCode, currentFilepath) + Colors.RESET) print('Checked {} files'.format(totalCheckdFilesCounter)) if filesWithErrorsCounter == 0: print('All the files match the Skyward formatting style') else: print(Colors.RED + '{:4.1f}% ({}/{}) of all analyzed files do not match Skyward formatting style!'.format( 100*filesWithErrorsCounter/totalCheckdFilesCounter, filesWithErrorsCounter, totalCheckdFilesCounter), Colors.RESET) # Exit if error if at least one file isn't correct if (filesWithErrorsCounter > 0): exit(-1) def find_in_code(directory, searchTerm, extensionFilters=('.cpp', '.h'), pathFilter=None): print( Colors.GREEN + 'Checking for \'{}\' in code files'.format(searchTerm) + Colors.RESET) # Statistics totalCheckdFilesCounter = 0 filesWithErrorsCounter = 0 # Walk through the directory and check each file for dirpath, dirnames, filenames in walk(directory): for filename in [f for f in filenames if f.endswith(extensionFilters) and (not pathFilter or dirpath.find(pathFilter) >= 0)]: totalCheckdFilesCounter += 1 # Prepare the complete filepath currentFilepath = join(dirpath, filename) # Check the current file with open(currentFilepath) as file: fileContent = file.read() # Check for linter off flag if re.search('linter off', fileContent, re.M): continue match = re.search(searchTerm, fileContent, re.M) if match: filesWithErrorsCounter += 1 # The current file has the error if (not args.quiet): print(Colors.YELLOW + 'Found \'{}\' in file {}'.format(searchTerm, currentFilepath) + Colors.RESET) print('Checked {} files'.format(totalCheckdFilesCounter)) if filesWithErrorsCounter == 0: print( 'All the files does not contain \'{}\''.format(searchTerm)) else: print(Colors.RED + '{:.1f}% ({}/{}) of all analyzed files contain \'{}\'!'.format( 100*filesWithErrorsCounter/totalCheckdFilesCounter, filesWithErrorsCounter, totalCheckdFilesCounter, searchTerm) + Colors.RESET) return filesWithErrorsCounter def check_find(directory): sum = find_in_code(directory, r'^using namespace', '.h') sum += find_in_code(directory, r'[^a-zA-Z0-9]printf\(', pathFilter='shared') sum += find_in_code(directory, '^ *throw ', pathFilter='catch') if sum > 0: exit(-1) def check_cppcheck(directory): print(Colors.GREEN + 'cppcheck' + Colors.RESET) # Run cppcheck on the directory try: result = check_output(['cppcheck', '-q', '--language=c++', '--template=gcc', '--std=c++11', '--enable=all', '--inline-suppr', '--suppress=unmatchedSuppression', '--suppress=unusedFunction', '--suppress=missingInclude', directory], stderr=STDOUT) # Parse results and count errors errors = re.findall(r'\[(\w+)\]', result.decode('utf-8')) errors = Counter(errors) if (not args.quiet): print('cppcheck found the following errors:') for error in errors: print('{:3} - {}'.format(errors[error], error)) totalErrors = sum(errors.values()) if (totalErrors > 0): print( Colors.RED + 'cppcheck found {} errors in total'.format(totalErrors) + Colors.RESET) exit(-1) else: print('cppcheck did not find any errors') except CalledProcessError as e: print(e.output.decode('utf-8')) exit(-1) # ------------------------------------------------------------- # MAIN # ------------------------------------------------------------- parser = config_cmd_parser() args = parser.parse_args() if (not args.directory): print('No directory specified') print('') parser.print_help() exit(-1) if (args.copyright): check_copyright(args.directory) if (args.format): check_format(args.directory) if (args.cppcheck): check_cppcheck(args.directory) if (args.find): check_find(args.directory) # Checks everything if no option is specified if (not args.copyright and not args.format and not args.find and not args.cppcheck): check_copyright(args.directory) check_format(args.directory) check_find(args.directory) check_cppcheck(args.directory)