def check_good_name(name): # good name good_name = filename # remove path from filename (if needed) if good_name.count("/") > 0: good_name = good_name[good_name.rfind("/") + 1:] if good_name.count("\\") > 0: good_name = good_name[good_name.rfind("\\") + 1:] # remove .h extension good_name = good_name[:good_name.find(".h")] # good name is all uppercase good_name = good_name.upper() # macros must not include '-' character, while files can # convert '-' to '_', which is valid for C++ preprocessor good_name = good_name.replace('-', '_') # a good name has format [PREFIX_]FILENAME_H good_name_re = re.compile("RAMSES_" + "([a-zA-Z0-9][a-zA-Z0-9]*_)*" + good_name + "_H$") valid = re.match(good_name_re, name) is not None if not valid: common.log_warning("check_header_guards", filename, 1, f"header guard '{name}' has invalid format, expected format: 'RAMSES_[PREFIX_]{good_name}_H'") return valid
def check_last_line_newline(filename, file_lines): """ Checks if file ends in newline """ if len(file_lines) > 0 and file_lines[-1] != "\n": common.log_warning("check_last_line_newline", filename, len(file_lines), "no newline at end of file")
def check_no_spacing_line_end(filename, file_lines): """ Checks that lines do not end with unnecessary white space characters """ for i in range(len(file_lines)): if re.search(" $", file_lines[i]): common.log_warning("check_no_spacing_line_end", filename, i + 1, "unneeded space(s) at end of line")
def check_tabs_no_spaces(filename, file_lines): """ Checks if any tab character "\t" exists in the file """ for i in range(len(file_lines)): if file_lines[i].count("\t") > 0: common.log_warning("check_tabs_no_spaces", filename, i + 1, "tab character (\\t) found, use 4 spaces instead", file_lines[i].strip(" "))
def check_doxygen(filename, file_contents, file_lines): """ Check doxygen files """ for line_number, line in enumerate(file_lines): if g_re_header_tag.search(line): cc.log_warning("check_doxygen", filename, line_number + 1, "usage html tag in doxygen header line", file_lines[line_number].strip(" \t\r\n"))
def check_license_for_file(file_name, file_contents, solution_path): """ Check license for given file name. """ license_re = G_RE_LICENSE_TEMPLATE_OPEN if not check_specific_license_in_file(file_name, file_contents, license_re): common.log_warning("check_license_in_file", file_name, 1, "no valid license found")
def check_file_attributes(filename): """ Check for unwanted file attributes """ mode = os.stat(filename).st_mode if bool(mode & stat.S_IXUSR) or bool(mode & stat.S_IXGRP) or bool(mode & stat.S_IXOTH): cc.log_warning("check_file_attributes", filename, 0, "may not have file executable bits set", "")
def check_curly_braces_alone_on_line(filename, file_contents, clean_file_contents, file_lines, clean_file_lines): """ Checks that there is no unnecessary code on same line with curly braces. """ # if: not at end re_if = re.compile(r'\W+if\s*\(.*\)\s*(?:\{|.*\}|.*;)$') # else: neither before nor after re_else = re.compile(r'}\s*else|else\s*{') # switch: not at end re_switch = re.compile(r'\W+switch\s*\(.*\)\s*(?:\{|.*\}|.*;)$') # while: before ok, not after re_while = re.compile(r'\W+while\s*\(.*\)\s*{') # for: not at end re_for = re.compile(r'\W+for\s*\(.*\)\s*(?:\{|.*\}|.*\)\s*;)$') # NOTE(tobias) not checking function definitions because too many false positives with lambdas, enums ... for i in range(len(clean_file_lines)): line = clean_file_lines[i] if re_if.search(line): common.log_warning( "check_curly_braces_alone_on_line", filename, i + 1, "curly brace or statement on same line with if", file_lines[i].strip(" ")) elif re_else.search(line): common.log_warning("check_curly_braces_alone_on_line", filename, i + 1, "curly brace on same line with else", file_lines[i].strip(" ")) elif re_switch.search(line): common.log_warning( "check_curly_braces_alone_on_line", filename, i + 1, "curly brace or statement on same line with switch", file_lines[i].strip(" ")) elif re_while.search(line): common.log_warning("check_curly_braces_alone_on_line", filename, i + 1, "curly brace after while", file_lines[i].strip(" ")) elif re_for.search(line): common.log_warning( "check_curly_braces_alone_on_line", filename, i + 1, "curly brace or statement on same line with for", file_lines[i].strip(" "))
def check_single_definition_on_line(filename, file_contents, clean_file_contents, file_lines, clean_file_lines): """ Checks if a line contains several variable definitions """ # remove arrays # an array regex starts with "{", ends with "}" and contains anything in between # an array of arrays is removed recursively inside out array_re = re.compile(r'\{[^ {};]*\}') clean_file_contents = common.clean_string_from_regex(clean_file_contents, array_re, '') # remove brackets array_brackets_re = re.compile(r'\[((?!\[|\]|;).)*\]') clean_file_contents = common.clean_string_from_regex(clean_file_contents, array_brackets_re, '') # remove angle brackets template_re = re.compile(r'<((?![<>;]).)*>') clean_file_contents = common.clean_string_from_regex(clean_file_contents, template_re, '') # variable name can be preceeded by * for pointers or & for references, can be followed by an assignment var_decl_re_text = r"""( (?:(?:&|\*+)\s*)? # can start by & (for reference defs) and/or any number of * (for pointer defs) \w+ # followed by identifier (?:\s*\([\w\s,]*\))? # can be followed by constructor call (?:\s*=\w+\s*|\s*\{[\w\s,]*\})? # or can be followed by an assignment of simple values, or value lists (for arrays) )""" # types can have qualifiers, be nested or in namespaces type_re_text = r"(?:const\s+|volatile\s+)?\w+(?:::\w+)*(?:&?|\**)" # it is enough to have several_defs_re_text = r""" \b\s*{0} \s+{1} (?:\s*,\s*{1})+; """.format(type_re_text, var_decl_re_text) several_defs_re = re.compile(several_defs_re_text, re.MULTILINE | re.VERBOSE) inside_for_header_re = re.compile(r"^\s*for \s*\(") for match in re.finditer(several_defs_re, clean_file_contents): line_number = clean_file_contents.count("\n", 0, match.end()) if re.search(inside_for_header_re, file_lines[line_number]) is None: common.log_warning("check_single_definition_on_line", filename, line_number + 1, "several definitions on same line", file_lines[line_number].strip(" \t\r\n"))
def check_license_for_file(file_name, file_contents, solution_path): """ Check license for given file name. Default ot open except it is in G_PROP_FILES list. """ license_re = G_RE_LICENSE_TEMPLATE_OPEN for pattern in G_PROP_FILES: full_pattern = os.path.join(solution_path, pattern) if file_name.startswith(full_pattern): license_re = G_RE_LICENSE_TEMPLATE_PROP break if not check_specific_license_in_file(file_name, file_contents, license_re): common.log_warning("check_license_in_file", file_name, 1, "no valid license found")
def check_single_statement_on_line(filename, file_contents, clean_file_contents, file_lines, clean_file_lines): """ Checks if there are several statements on the same line """ open_parantheses_count = 0 semicolon_inside_oneline_lambda_re = re.compile(r'\[.*\].*\{.*;.*\}') for i in range(len(file_lines)): line = clean_file_lines[i] open_parantheses_count += line.count("(") - line.count(")") # check if there are several semicolons on the line and that is not in the middle of a for statement if line.count(";") > 1 and open_parantheses_count == 0: # check that there is NO right parentheses after semicolon if line.rfind(";") > line.rfind(")") and not re.search(semicolon_inside_oneline_lambda_re, line): common.log_warning("check_single_statement_on_line", filename, i + 1, "several statements on same line", file_lines[i].strip(" "))
def check_header_guards(filename, file_contents): """ Check if a header files contains valid header guards: 1- #ifndef GUARDNAME directly after license that has a guard name identical to file name in uppercase letters, underscores at beginnings and end are tollerable 2- directly followed by #define that matches the guard name of #ifndef exactly 3- the file has to end with an #endif """ is_h_file = filename[-2:] == ".h" is_hpp_file = filename[-4:] == ".hpp" if not (is_h_file or is_hpp_file): return # remove strings string_re = re.compile(r'"((?!((?<!((?<!\\)\\))")).)*"', re.DOTALL) file_contents = common.clean_string_from_regex(file_contents, string_re, 'x') # remove multi-line comments ml_comment_re = re.compile(r'(\s*)/\*((?!\*/).)*\*/', re.DOTALL) file_contents = common.clean_string_from_regex(file_contents, ml_comment_re, '') # remove single line comments sl_comment_re = re.compile("//.*$", re.MULTILINE) file_contents = re.sub(sl_comment_re, '', file_contents) # remove any padding white space characters file_contents = file_contents.strip(" \t\n\f\r") # find the pragma once guard pragma_re = re.compile(r"(\s*)#pragma(\s+)once(\s*)", re.MULTILINE) pragma_match = re.findall(pragma_re, file_contents) if len(pragma_match) == 0: common.log_warning("check_header_guards", filename, 1, "header guard missing") if len(pragma_match) > 1: common.log_warning("check_header_guards", filename, 1, "more than one occurance of header guard found") return None
def check_enum_style(filename, clean_file_contents): """ Check that enums have - E prefix on name if old enum - name is CamelCase - enumerators have name prefix if old enum - enumerators have no name prefix if enum class """ # enum [class] <name> [: maybe type] { <values> } enum_re = re.compile( r'(?<!\w)enum\s+(class\s+)?(\w+)\s*(?::\s*\w+\s*])?{((\s|\S)*?)}') for enum_match in re.finditer(enum_re, clean_file_contents): line_number = clean_file_contents[:enum_match.start()].count("\n") g = enum_match.groups() is_enum_class = g[0] is not None enum_name = g[1] enum_values = [ m for m in [n.strip() for n in g[2].split('\n')] if m != '' ] # old enum must start with an 'E' if not is_enum_class and not enum_name.startswith('E'): common.log_warning("check_enum_style", filename, line_number, "enum must begin with 'E': " + enum_name) # must be camel case if enum_name.upper() == enum_name: common.log_warning("check_enum_style", filename, line_number, "enum must be CamelCase: " + enum_name) # old enum has prefix on values, enum class does NOT have name prefix for v in enum_values: if is_enum_class: if v.startswith(enum_name): common.log_warning( "check_enum_style", filename, line_number, "enum class value may not begin with " + enum_name + ": " + v) else: if not v.startswith(enum_name): common.log_warning( "check_enum_style", filename, line_number, "enum value must begin with " + enum_name + "_ : " + v)
def check_deprecated(filename, file_contents, clean_file_contents, file_lines, clean_file_lines): """ Check for usage of deprecated constructs """ # check for old mock syntax for line_number, line in enumerate(clean_file_lines): if g_re_deprecated_mock_syntax.search(line): common.log_warning("check_deprecated", filename, line_number + 1, "usage of old googletest mock syntax", file_lines[line_number].strip(" \t\r\n")) # check for unwanted includes includes for line_number, line in enumerate(file_lines): if g_re_unwanted_gtest_include.search(line): common.log_warning("check_deprecated", filename, line_number + 1, "usage of non-standard gtest include, use '#include \"gtest/gtest.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_gmock_include.search(line): common.log_warning("check_deprecated", filename, line_number + 1, "usage of non-standard gmock include, use '#include \"gmock/gmock.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_fmt_include.search(line): common.log_warning("check_deprecated", filename, line_number + 1, "usage of unwanted fmt include, use '#include \"fmt/format.h\"' instead", file_lines[line_number].strip(" \t\r\n"))
def check_header_guards(filename, file_contents): """ Check if a header files contains valid header guards: 1- #ifndef GUARDNAME directly after license that has a guard name identical to file name in uppercase letters, underscores at beginnings and end are tollerable 2- directly followed by #define that matches the guard name of #ifndef exactly 3- the file has to end with an #endif """ is_h_file = filename[-2:] == ".h" is_hpp_file = filename[-4:] == ".hpp" if not (is_h_file or is_hpp_file): return # remove strings string_re = re.compile(r'"((?!((?<!((?<!\\)\\))")).)*"', re.DOTALL) file_contents = common.clean_string_from_regex(file_contents, string_re, 'x') # remove multi-line comments ml_comment_re = re.compile(r'(\s*)/\*((?!\*/).)*\*/', re.DOTALL) file_contents = common.clean_string_from_regex(file_contents, ml_comment_re, '') # remove single line comments sl_comment_re = re.compile("//.*$", re.MULTILINE) file_contents = re.sub(sl_comment_re, '', file_contents) # remove any padding white space characters file_contents = file_contents.strip(" \t\n\f\r") def check_good_name(name): # good name good_name = filename # remove path from filename (if needed) if good_name.count("/") > 0: good_name = good_name[good_name.rfind("/") + 1:] if good_name.count("\\") > 0: good_name = good_name[good_name.rfind("\\") + 1:] # remove .h extension good_name = good_name[:good_name.find(".h")] # good name is all uppercase good_name = good_name.upper() # macros must not include '-' character, while files can # convert '-' to '_', which is valid for C++ preprocessor good_name = good_name.replace('-', '_') # a good name has format [PREFIX_]FILENAME_H good_name_re = re.compile("RAMSES_" + "([a-zA-Z0-9][a-zA-Z0-9]*_)*" + good_name + "_H$") valid = re.match(good_name_re, name) is not None if not valid: common.log_warning("check_header_guards", filename, 1, f"header guard '{name}' has invalid format, expected format: 'RAMSES_[PREFIX_]{good_name}_H'") return valid # try pragma once and early out if found pragma_once_re = re.compile(r"(\s*)#pragma once\n", re.MULTILINE) pragma_once_match = re.match(pragma_once_re, file_contents) if pragma_once_match is not None: return None # find the ifndef guard ifndef_re = re.compile(r"(\s*)#ifndef .*", re.MULTILINE) ifndef_match = re.match(ifndef_re, file_contents) if ifndef_match is None: common.log_warning("check_header_guards", filename, 1, "header guard missing") return None # get guard name ifndef_match_text = file_contents[:ifndef_match.end()] ifndef_match_text = ifndef_match_text.strip(" \n\r\f\t") splitted_match_text = ifndef_match_text.split(" ") ifndef_guard_name = splitted_match_text[1] if len(splitted_match_text) > 1 else "" if not check_good_name(ifndef_guard_name): return None # check uniqueness if ifndef_guard_name in g_header_guard_string_map: common.log_warning("check_header_guards", filename, 1, f"Header guard name '{ifndef_guard_name}' already used in file {g_header_guard_string_map[ifndef_guard_name]}") return None g_header_guard_string_map[ifndef_guard_name] = filename file_contents = file_contents[ifndef_match.end():] # find the define guard define_re = re.compile(r"(\s*)#define .*") define_match = re.match(define_re, file_contents) if define_match is None: common.log_warning("check_header_guards", filename, 1, f"header file does not contain a #define guard at the beginning that matches #ifndef {ifndef_guard_name}") return None # get guard name define_match_text = file_contents[:define_match.end()] define_match_text = define_match_text.strip(" \n\r\f\t") splitted_match_text = define_match_text.split(" ") define_guard_name = splitted_match_text[1] if len(splitted_match_text) > 1 else "" # check if #ifndef and #define have identical guard names if define_guard_name != ifndef_guard_name: common.log_warning("check_header_guards", filename, 1, f"header guard names do not match :(#ifndef {ifndef_guard_name}) and (#define {define_guard_name})") # find the endif guard endif_re = re.compile(r"#endif.*(\s*)$") endif_match = re.search(endif_re, file_contents) if endif_match is None: common.log_warning("check_header_guards", filename, 1, "header file does not end with #endif at last line") return None
def check_deprecated(filename, file_contents, clean_file_contents, file_lines, clean_file_lines): """ Check for usage of deprecated constructs """ # check for old mock syntax for line_number, line in enumerate(clean_file_lines): if g_re_deprecated_mock_syntax.search(line): common.log_warning("check_deprecated", filename, line_number + 1, "usage of old googletest mock syntax", file_lines[line_number].strip(" \t\r\n")) # check for unwanted includes includes for line_number, line in enumerate(file_lines): if g_re_unwanted_gtest_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, "usage of non-standard gtest include, use '#include \"gtest/gtest.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_gmock_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, "usage of non-standard gmock include, use '#include \"gmock/gmock.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_fmt_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, "usage of unwanted fmt include, use '#include \"fmt/format.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_system_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, 'found system include with "...", use <...> instead"', file_lines[line_number].strip(" \t\r\n")) if filename.endswith('.h') and g_re_unwanted_force_thread_local.search( line): common.log_warning( "check_deprecated", filename, line_number + 1, 'found disallowed #include "Utils/ThreadLocalLogForced.h" in header"', file_lines[line_number].strip(" \t\r\n")) # TODO: Fix offenders and reduce limit until some reasonable length is reached if len(line) > g_max_line_length: common.log_warning( "check_deprecated", filename, line_number + 1, f'line of {len(line)} characters too long (max allowed {g_max_line_length}), add some linebreaks ', file_lines[line_number].strip(" \t\r\n"))
def check_api_export_symbols(filename, clean_file_contents): """ Check that public API symbols are exported - A class has RLOGIC_API prefix before the name, structs and enums don't have it """ is_api = ("include/public" in filename) is_header = (filename[-2:] == ".h") or (filename[-4:] == ".hpp") is_api_header = is_api and is_header if is_api_header: # symbol_def_re matches following patterns: # (correct case) class|struct RLOGIC_API SymbolName { [...] # (correct case) class|struct RLOGIC_API SymbolName : [...] # (correct case) enum|enum class SymbolName : [...] # (wrong case) class|struct SymbolName { [...] # (wrong case) class|struct SymbolName : [...] symbol_def_re = re.compile( r'(template [^;]+?)?\s+(enum|class|struct|enum\s+class)(\s+)(\w+)(\s+)(\w*)(\s*)(\{|\:)' ) for symbol_match in re.finditer(symbol_def_re, clean_file_contents): line_number = clean_file_contents[:symbol_match.start()].count( "\n") symbolNameGroups = symbol_match.groups() isTemplate = symbolNameGroups[0] is not None isEnum = symbolNameGroups[1].strip() == "enum" isStruct = symbolNameGroups[1].strip() == "struct" isEnumClass = "enum " in symbolNameGroups[1].strip() firstWord = symbolNameGroups[3].strip() secondWord = symbolNameGroups[5].strip() # check special cases that should NOT have RLOGIC_API if isEnum: if firstWord == "RLOGIC_API": common.log_warning( "check_api_export_symbols", filename, line_number + 1, "Enum exported as RLOGIC_API: " + secondWord) elif isEnumClass: if firstWord == "RLOGIC_API": common.log_warning( "check_api_export_symbols", filename, line_number + 1, "Enum class exported as RLOGIC_API: " + secondWord) elif isStruct: if firstWord == "RLOGIC_API": common.log_warning( "check_api_export_symbols", filename, line_number + 1, "Struct exported as RLOGIC_API: " + secondWord) elif isTemplate: if firstWord == "RLOGIC_API": common.log_warning( "check_api_export_symbols", filename, line_number + 1, "Template exported as RLOGIC_API: " + secondWord) else: # remaining cases should have RLOGIC_API if firstWord != "RLOGIC_API": common.log_warning( "check_api_export_symbols", filename, line_number + 1, "Public symbol not exported as RLOGIC_API: " + firstWord) else: # Will find occurances of RLOGIC_API surrounded by space(s) RLOGIC_API_re = re.compile(r'(?<!\w)(RLOGIC_API)\s') for symbol_match in re.finditer(RLOGIC_API_re, clean_file_contents): line_number = clean_file_contents[:symbol_match.start()].count( "\n") common.log_warning( "check_api_export_symbols", filename, line_number + 1, "Exporting API symbol in a non-API-header file! This symbol will be unusable." )
def check_deprecated(filename, file_contents, clean_file_contents, file_lines, clean_file_lines): """ Check for usage of deprecated constructs """ # check for old mock syntax for line_number, line in enumerate(clean_file_lines): if g_re_deprecated_mock_syntax.search(line): common.log_warning("check_deprecated", filename, line_number + 1, "usage of old googletest mock syntax", file_lines[line_number].strip(" \t\r\n")) # check for unwanted includes includes for line_number, line in enumerate(file_lines): if g_re_unwanted_gtest_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, "usage of non-standard gtest include, use '#include \"gtest/gtest.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_gmock_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, "usage of non-standard gmock include, use '#include \"gmock/gmock.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_fmt_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, "usage of unwanted fmt include, use '#include \"fmt/format.h\"' instead", file_lines[line_number].strip(" \t\r\n")) if g_re_unwanted_system_include.search(line): common.log_warning( "check_deprecated", filename, line_number + 1, 'found system include with "...", use <...> instead"', file_lines[line_number].strip(" \t\r\n")) if len(line) > g_max_line_length: common.log_warning( "check_deprecated", filename, line_number + 1, f'line of {len(line)} characters too long (max allowed {g_max_line_length}), add some linebreaks ', file_lines[line_number].strip(" \t\r\n"))
def check_correct_space_count(filename, file_lines): """ Checks that the number of spaces at the beginning of every line is divisible by four """ previous_indent = 0 in_multiline_comment = False valid_indents_stack = [] for i in range(len(file_lines)): if in_multiline_comment: # if end of muli-line comment if file_lines[i].count("*/") > 0: # just make sure there is no other new multi-line comment starting on the same line (after the current mult-line comment is closed) in_multiline_comment = file_lines[i].count( "/*") > 0 and file_lines[i].index( "/*") > file_lines[i].index("*/") else: in_multiline_comment = file_lines[i].count("/*") > 0 # regex searches for the first character that is not a space character found_match = re.search("(?! )", file_lines[i]) if found_match: space_count = found_match.start() while space_count > 0 and valid_indents_stack and valid_indents_stack[ -1] > space_count: valid_indents_stack.pop() # check by indent stack if space_count in valid_indents_stack and space_count == valid_indents_stack[ -1]: ok_by_stack = True else: ok_by_stack = False # check divisible by 4 OR same indent as previous line or at least properly +-4*x from previous line warned = False if not ok_by_stack and (space_count % 4 != 0) and ( previous_indent != space_count) and ( abs(previous_indent - space_count) % 4 != 0): # allow indent to parenthesis in previous line is_smart_indent = False if (i > 0) and (space_count > 0) and (len( file_lines[i - 1]) > space_count - 1): prev_line = file_lines[i - 1] cur_line_without_spaces = file_lines[i][space_count:] if prev_line[space_count - 1] in [ '(', '[' ] or prev_line[space_count - 4] in ['(', '[']: is_smart_indent = True elif prev_line.endswith(",") or prev_line.endswith("<<") or prev_line.endswith("||") or prev_line.endswith("&&") or \ cur_line_without_spaces.startswith("<<") or cur_line_without_spaces.startswith(">>"): is_smart_indent = True if not is_smart_indent: common.log_warning( "check_correct_space_count", filename, i + 1, "number of spaces at beginning of line must be divisible by 4 (or conform to smart indent)" ) warned = True # only previous spaces when not zero if space_count != 0: # add to stack when valid and not already in if not warned: if not valid_indents_stack: valid_indents_stack.append(space_count) elif valid_indents_stack[-1] != space_count: valid_indents_stack.append(space_count) previous_indent = space_count else: previous_indent = 0